Compare commits

..

30 Commits

Author SHA1 Message Date
Géry Debongnie 46095b7a96 [ADD] playground: add monkey patching example
closes #312
2019-10-05 09:00:23 +02:00
Géry Debongnie c3453d35b9 [IMP] hooks/component: remove updateEnv, add useSubEnv
closes #182
2019-10-04 22:54:27 +02:00
Géry Debongnie b0dd0252a4 [REF] component: move method at proper place 2019-10-04 22:54:27 +02:00
Géry Debongnie e2b2e54e29 [DOC] tooling: add missing link 2019-10-04 22:12:56 +02:00
Géry Debongnie 766c378ac9 [DOC] hooks: add missing doc for onWillPatch/onPatched 2019-10-03 11:57:58 +02:00
Géry Debongnie 022b29b6a0 [IMP] component/qweb: remove t-mounted directive
The t-mounted directive main goals can be achieved with hooks, in a
better and more intuitive way.

closes #308
2019-10-03 10:57:24 +02:00
Géry Debongnie 367725d194 [IMP] hooks: add onWillPatch and onPatched
This commit also changes the behaviour of willPatch and patched: they no
longer transfer data from one to the other, because it can be done
cleanly by hooks and a closure.

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

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

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

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

      const SUBTEMPLATE = xml`<span><t t-esc="state.n"/></span>`
      class Parent extends Widget {
          static template = xml`<div><t t-call="${SUBTEMPLATE}"/></div>`
          state = {n: 42};
      }
2019-09-21 23:05:44 +02:00
Géry Debongnie cc82b3ffcd [IMP] component: support dynamic t-component 2019-09-21 21:58:19 +02:00
Géry Debongnie bbbaf95c8e Merge pull request #296 from odoo/issue-293-owl-fibers
[MERGE] component: introduce owl fiber
2019-09-20 13:54:59 +02:00
Géry Debongnie caa63628dc [REF] component: refactor fiber creation methods 2019-09-20 11:14:50 +02:00
Géry Debongnie 7a49b9d94e [REF] component: move render props and promise to fiber
part of #293
2019-09-20 10:48:10 +02:00
Géry Debongnie 01eb338e69 [REF] component: move patchqueue management to fiber
part of #293
2019-09-20 10:47:58 +02:00
Géry Debongnie 3048c6f961 [REF] component: introduce fiber
part of #293
2019-09-20 10:47:55 +02:00
Géry Debongnie 8edc637033 [FIX] qweb: remove comments in xml strings before parsing
It was done in loadTemplates, but this does not work if we want to add
templates from another source.
2019-09-19 15:18:59 +02:00
Géry Debongnie c9910077a4 [FIX] qweb: do not capture current qweb instance in closure
it is not needed, and has a bad effect on globally registered templates.
2019-09-12 23:23:55 +02:00
Géry Debongnie 6bf70f016c [DOC] doc: update readme.md 2019-09-12 23:23:39 +02:00
Géry Debongnie 905619f80f [ADD] tools: add benchmark for 0.21.0 2019-09-12 14:50:56 +02:00
52 changed files with 6989 additions and 1313 deletions
+14
View File
@@ -0,0 +1,14 @@
Most of the files are
Copyright (c) 2004-2015 Odoo S.A.
Many files also contain contributions from third
parties. In this case the original copyright of
the contributions can be traced through the
history of the source version control system.
When that is not the case, the files contain a prominent
notice stating the original copyright and applicable
license, or come with their own dedicated COPYRIGHT
and/or LICENSE file.
+172 -24
View File
@@ -1,26 +1,32 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
_A web framework for structured, dynamic and maintainable applications_
_A no nonsense web framework for structured, dynamic and maintainable applications_
## Project Overview
The Odoo Web Library (OWL) is a small (~16kb gzipped) UI framework intended to be the basis for
the [Odoo](https://www.odoo.com/) Web Client, and hopefully many other Odoo
related projects. OWL's main features are:
The Odoo Web Library (OWL) is a smallish (~17kb gzipped) UI framework intended to
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
framework, written in Typescript, taking the best ideas from React and Vue in a
simple and consistent way. Owl's main features are:
- a _declarative component system_, (template based, with asynchronous rendering and a virtual dom)
- a store implementation (for state management)
- a declarative component system,
- a reactivity system based on hooks,
- a store implementation (for state management),
- a small frontend router
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the OWL framework.
Owl components are defined with ES6 classes, they use QWeb templates, an underlying
virtual dom, integrates beautifully with hooks, and the rendering is asynchronous.
**Try it online!** An online playground is available at [https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground) to let you experiment with the Owl framework. There
are some code examples to showcase some interesting features.
## Example
Here is a short example to illustrate interactive components:
```javascript
import { Component, QWeb } from 'owl'
import { xml } from 'owl/tags'
import { Component, QWeb, useState } from "owl";
import { xml } from "owl/tags";
class Counter extends Component {
static template = xml`
@@ -28,7 +34,7 @@ class Counter extends Component {
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 };
state = useState({ value: 0 });
increment() {
this.state.value++;
@@ -43,17 +49,20 @@ class App extends Component {
</div>`;
static components = { Counter };
}
const app = new App({ qweb: new QWeb() });
app.mount(document.body);
```
Note that the counter component is made reactive with the [`useState`](doc/hooks.md#usestate)
hook. Also, all examples here uses the `xml` helper to define inline templates.
But this is not mandatory, many applications will load templates separately.
More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application.
## OWL's Design Principles
## Design Principles
OWL is designed to be used in highly dynamic applications where changing
requirements are common and code needs to be maintained by large teams.
@@ -82,7 +91,7 @@ The complete documentation can be found [here](doc/readme.md). The most importan
- [Quick Start](doc/quick_start.md)
- [Component](doc/component.md)
- [QWeb](doc/qweb.md)
- [Hooks](doc/hooks.md)
Found an issue in the documentation? A broken link? Some outdated information?
Submit a PR!
@@ -91,20 +100,159 @@ Submit a PR!
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-0.21.0.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.js)
- [owl-0.21.0.min.js](https://github.com/odoo/owl/releases/download/v0.21.0/owl.min.js)
- [owl-0.22.0.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.js)
- [owl-0.22.0.min.js](https://github.com/odoo/owl/releases/download/v0.22.0/owl.min.js)
Some npm scripts are available:
| Command | Description |
| --------------------- | ---------------------------------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all tests |
| `npm run test:watch` | run all tests, and keep a watcher |
| `npm run tools` | build tools applications, start a static server (see [here](doc/tooling.md)) |
| `npm run tools:watch` | same as `tools`, but with a watcher to rebuild owl |
| Command | Description |
| ---------------- | -------------------------------------------------- |
| `npm install` | install every dependency required for this project |
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
| `npm run minify` | minify the prebuilt owl.js file |
| `npm run test` | run all (owl) tests |
## Quick Overview
Owl components in an application are used to define a (dynamic) tree of components.
```
Root
/ \
A B
/ \
C D
```
**Environment:** the root component is special: it is created with an environment,
which should contain a `QWeb` instance. The environment is then automatically
propagated to each sub components (and accessible in the `this.env` property).
```js
const env = { qweb: new QWeb() };
const app = new App(env);
app.mount(document.body);
```
The environment is mostly static. Each application is free to add anything to
the environment, which is very useful, since this can be accessed by each sub
component. Some good use case for that is some configuration keys, session
information or generic services (such as doing rpcs, or accessing local storage).
Doing it this way means that components are easily testable: we can simply
create a test environment with mock services.
**State:** each component can manage its own local state. It is a simple ES6
class, there are no special rules:
```js
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 };
increment() {
this.state.value++;
this.render();
}
}
```
The example above shows a component with a local state. Note that since there
is nothing magical to the `state` object, we need to manually call the `render`
function whenever we update it. This can quickly become annoying (and not
efficient if we do it too much). There is a better way: using the `useState`
hook, which transforms an object into a reactive version of itself:
```js
const { useState } = owl.hooks;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
**Props:** sub components often needs some information from their parents. This
is done by adding the required information to the template. This will then be
accessible by the sub component in the `props` object. Note that there is an
important rule here: the information contained in the `props` object is not
owned by the sub component, and should never be modified.
```js
class Child extends Component {
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child name="'Owl'" />
<Child name="'Framework'" />
</div>`;
static components = { Child };
}
```
**Communication:** there are multiple ways to communicate information between
components. However, the two most important ways are the following:
- from parent to children: by using `props`,
- from a children to one of its parent: by triggering events.
The following example illustrate both mechanisms:
```js
class OrderLine extends Component {
static template = xml`
<div t-on-click="add">
<div><t t-esc="props.line.name"/></div>
<div>Quantity: <t t-esc="props.line.quantity"/></div>
</div>`;
add() {
this.trigger("add-to-order", { line: props.line });
}
}
class Parent extends Component {
static template = xml`
<div t-on-add-to-order="addToOrder">
<OrderLine
t-foreach="orders"
t-as="line"
line="line" />
</div>`;
static components = { OrderLine };
orders = useState([{ id: 1, name: "Coffee", quantity: 0 }, { id: 2, name: "Tea", quantity: 0 }]);
addToOrder(event) {
const line = event.detail.line;
line.quantity++;
}
}
```
In this example, the `OrderLine` component trigger a `add-to-order` event. This
will generate a DOM event which will bubble along the DOM tree. It will then be
intercepted by the parent component, which will then get the line (from the
`detail` key) and then increment its quantity. See the section on [event handling](doc/component.md#event-handling)
for more details on how events work.
Note that this example would have also worked if the `OrderLine` component
directly modifies the `line` object. However, this is not a good practice: this
only works because the `props` object received by the child component is reactive,
so the child component is then coupled to the parents implementation.
## License
+193 -89
View File
@@ -5,6 +5,7 @@
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Reactive System](#reactive-system)
- [Properties](#properties)
- [Static Properties](#static-properties)
- [Methods](#methods)
@@ -15,13 +16,14 @@
- [Event Handling](#event-handling)
- [Form Input Bindings](#form-input-bindings)
- [`t-key` Directive](#t-key-directive)
- [`t-mounted` Directive](#t-mounted-directive)
- [Semantics](#semantics)
- [Props Validation](#props-validation)
- [References](#references)
- [Slots](#slots)
- [Asynchronous Rendering](#asynchronous-rendering)
- [Error Handling](#error-handling)
- [Functional Components](#functional-components)
- [SVG components](#svg-components)
## Overview
@@ -45,16 +47,15 @@ exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in Q
Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md).
## Example
Let us have a look at a simple component:
```javascript
const { useState } = owl.hooks;
class ClickCounter extends owl.Component {
state = { value: 0 };
state = useState({ value: 0 });
increment() {
this.state.value++;
@@ -74,9 +75,8 @@ latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the
Component class. If no static `template` key is defined, then
Owl will use the component's name as template name. Here,
a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any
change to it will cause a rerendering.
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
[observed](observer.md), and any change to it will cause a rerendering.
## Reference
@@ -89,6 +89,25 @@ Be aware that the name of the component may be significant: if a component does
not define a `template` key, then Owl will lookup in QWeb to
find a template with the component name (or one of its ancestor).
### Reactive system
OWL components can be made reactive by observing some part of their state. See the [hooks](hooks.md) section for more details.
The main idea is that the `useState` hook generate a proxy version of an object
(this is done by an [observer](observer.md)), which allows the component to
react to any change.
```javascript
const { useState } = owl.hooks;
class SomeComponent extends owl.Component {
state = useState({ a: 0, b: 1 });
}
```
Note that there is an important limitation: hooks need to be called in the
constructor.
### Properties
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
@@ -96,18 +115,11 @@ find a template with the component name (or one of its ancestor).
- **`env`** (Object): the component environment, which contains a QWeb instance.
- **`state`** (Object): this is the location of the component's state, if there is
any. After the willStart method, the `state` property is observed, and each
change will cause the component to rerender itself.
- **`props`** (Object): this is an object given (in the constructor) by the parent
to configure the component. It can be dynamically changed later by the parent,
in some case. Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component!!
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
or sub components defined by a `t-ref` directive in the component's template.
### Static Properties
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
@@ -236,7 +248,7 @@ component.
```javascript
constructor(parent, props) {
super(parent, props);
this.state = {someValue: true};
this.state = useState({someValue: true});
this.template = 'mytemplate';
}
```
@@ -246,7 +258,7 @@ implemented in most cases:
```javascript
class ClickCounter extends owl.Component {
state = { value: 0 };
state = useState({ value: 0 });
...
}
@@ -271,9 +283,6 @@ At this point, the component is not yet rendered. Note that a slow `willStart` m
interface. Therefore, some care should be made to make this method as
fast as possible.
After the `willStart` method is completed, the state will be observed with a
new `Observer`. Then, the component will be rendered by `QWeb`.
#### `mounted()`
`mounted` is called each time a component is attached to the
@@ -287,10 +296,9 @@ always be unmounted at some point in the future.
The mounted method will be called recursively on each of its children. First,
the parent, then all its children.
Note that the state is now observed. It is however allowed (but not encouraged)
to modify the state in the `mounted` hook. Doing so will cause a rerender,
which will not be perceptible by the user, but will slightly slow down the
component.
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
Doing so will cause a rerender, which will not be perceptible by the user, but
will slightly slow down the component.
#### `willUpdateProps(nextProps)`
@@ -315,14 +323,11 @@ It is not called on the initial render. This is useful to read some
information from the DOM. For example, the current position of the
scrollbar.
Note that modifying the state object is not allowed here. This method is called just
Note that modifying the state is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM (this can
happen with components with `t-keepalive`).
The return value of this method will be given as the first argument of the
corresponding `patched` call.
#### `patched(snapshot)`
This hook is called whenever a component did actually update its DOM (most
@@ -333,8 +338,6 @@ with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the compoent is
not in the DOM (this can happen with components with `t-keepalive`).
The `snapshot` parameter is the result of the previous `willPatch` call.
Updating the compoent state in this hook is possible, but not encouraged.
One need to be careful, because updates here will cause rerender, which in
turn will cause other calls to patched. So, we need to be particularly
@@ -397,9 +400,9 @@ easy to test a component.
Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to trigger the
`update` event on the QWeb instance. For example, a responsive environment
could be programmed like this:
themselves whenever the environment is modified: one only needs to call the
`forceUpdate` method on the QWeb instance. For example, a responsive environment
could be done like this:
```js
function setupResponsivePlugin(env) {
@@ -408,7 +411,7 @@ function setupResponsivePlugin(env) {
const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile;
env.qweb.trigger("update");
env.qweb.forceUpdate();
}
}, 15);
window.addEventListener("resize", updateEnv);
@@ -442,7 +445,8 @@ dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`.
Note that the rendering context for the template is the component itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the component.
that the template can access `state` (if it exists), `props`, `env`, or any
methods defined in the component.
```xml
<div t-name="ParentComponent">
@@ -453,7 +457,7 @@ that the template can access `state`, `props`, `env`, or any methods defined in
```js
class ParentComponent {
static components = { ChildComponent };
state = { val: 4 };
state = useState({ val: 4 });
}
```
@@ -502,6 +506,31 @@ class ParentComponent {
}
```
There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component:
```js
class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`;
}
class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`;
}
class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" };
get myComponent() {
return this.state.child === "a" ? A : B;
}
}
```
In this example, the component `App` selects dynamically the concrete sub
component class.
**CSS and style:** there is some specific support to allow the parent to declare
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
root component element.
@@ -617,7 +646,7 @@ form!). A possible way to do this is to do it by hand:
```js
class Form extends owl.Component {
state = { text: "" };
state = useState({ text: "" });
_updateInputValue(event) {
this.state.text = event.target.value;
@@ -637,8 +666,9 @@ plumbing code is slightly different if you need to interact with a checkbox,
or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value
is the (top-level) name in the state object. With the `t-model` directive, we
can write a shorter code, equivalent to the previous example:
should be an observed value in the component (usually `state.someValue`). With
the `t-model` directive, we can write a shorter code, equivalent to the previous
example:
```js
class Form extends owl.Component {
@@ -648,7 +678,7 @@ class Form extends owl.Component {
```xml
<div>
<input t-model="text" />
<input t-model="state.text" />
<span t-esc="state.text" />
</div>
```
@@ -658,11 +688,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
```xml
<div>
<div>Text in an input: <input t-model="someVal"/></div>
<div>Textarea: <textarea t-model="otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></div>
<div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection:
<select t-model="color">
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
@@ -671,11 +701,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="color"/>
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="color" />
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label>
</span>
</div>
@@ -693,7 +723,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
For example:
```xml
<input t-model.lazy="someVal" />
<input t-model.lazy="state.someVal" />
```
These modifiers can be combined. For instance, `t-model.lazy.number` will only
@@ -725,24 +755,6 @@ There are three main use cases:
- _animations_: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### `t-mounted` Directive
The `t-mounted` directive allows to register a callback to execute whenever the node
is inserted into the DOM.
```xml
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
```
```js
class MyComponent extends owl.Component {
...
focusMe() {
this.refs.someInput.focus();
}
}
```
### Semantics
We give here an informal description of the way components are created/updated
@@ -927,44 +939,60 @@ Examples:
### References
The `t-ref` directive helps a component keep reference to some inside part of it.
Like the `t-on` directive, it can work either on a DOM node, or on a component:
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
more detail.
As a short example, here is how we could set the focus on a given input:
```xml
<div>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
<input t-ref="input"/>
<button t-on-click="focusInput">Click</button>
</div>
```
In this example, the component will be able to access the `div` and the component
inside the special `refs` variable:
```js
this.refs.someDiv;
this.refs.someComponent;
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
```
This is useful for various usecases: for example, integrating with an external
library that needs to render itself inside an actual DOM node. Or for calling
some method on a sub component.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-component` directives). For example, if we have
`id` set to 44 in the rendering context,
The `useRef` hook can also be used to get a reference to an instance of a sub
component rendered by Owl. In that case, we need to access it with the `comp`
property instead of `el`:
```xml
<div t-ref="component_{{id}}"/>
<div>
<SubComponent t-ref="sub"/>
<button t-on-click="doSomething">Click</button>
</div>
```
```js
this.refs.component_44;
import { useRef } from "owl/hooks";
class SomeComponent extends Component {
static components = { SubComponent };
subRef = useRef("sub");
doSomething() {
this.subRef.comp.doSomeThingElse();
}
}
```
Note that these two examples uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget to access
it with the `el` or `comp` suffix.
### Slots
To make generic components, it is useful to be able for a parent component to _inject_
@@ -1092,8 +1120,8 @@ For example, here is how we could implement an `ErrorBoundary` component:
```
```js
class ErrorBoundary extends Widget {
state = { error: false };
class ErrorBoundary extends Component {
state = useState({ error: false });
catchError() {
this.state.error = true;
@@ -1120,3 +1148,79 @@ env.qweb.on("error", null, function(error) {
// react to the error
});
```
### Functional Components
Owl does not exactly have functional components. However, there is an extremely
close alternative: calling sub templates.
A stateless functional component in react is usually some kind of function that
maps props to a virtual dom (often with `jsx`). So, basically, almost like a
template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object:
```js
const Welcome = xml`<h1>Hello, {props.name}</h1>`;
class MyComponent extends Component {
static template = xml`
<div>
<t t-call=${Welcome}/>
<div>something</div>
</div>
`;
}
```
The way this works is that sub templates are inlined, and have access to the
ambient context. They can therefore access `props`, and any other part of the
caller component.
### SVG Components
Owl components can be used to generate dynamic SVG graphs:
```js
class Node extends Component {
static template = xml`
<g>
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
<t t-set="childx" t-value="props.x + 100"/>
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
<t t-foreach="props.node.children || []" t-as="child">
<t t-set="childy" t-value="props.y + child_index*height"/>
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
<Node x="childx" y="childy" node="child" height="height"/>
</t>
</g>
`;
static components = { Node };
}
class RootNode extends Component {
static template = xml`
<svg height="180">
<Node node="graph" x="10" y="20" height="180"/>
</svg>
`;
static components = { Node };
graph = {
label: "a",
children: [
{ label: "b" },
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
{ label: "f", children: [{ label: "g" }] }
]
};
}
```
This `RootNode` component will then display a live SVG representation of the
graph described by the `graph` property. Note that there is a recursive structure
here: the `Node` component uses itself as a subcomponent.
Note that since SVG needs to be handled in a specific way (its namespace needs
to be properly set), there is a small constraint for Owl components: if an owl
component is supposed to be a part of an svg graph, then its root node needs to
be a `g` tag, so Owl can properly set the namespace.
+275
View File
@@ -0,0 +1,275 @@
# 🦉 Hooks 🦉
## Content
- [Overview](#overview)
- [Example: Mouse Position](#example-mouse-position)
- [Example: Autofocus](#example-autofocus)
- [Reference](#reference)
- [One Rule](#one-rule)
- [`useState`](#usestate)
- [`onMounted`](#onmounted)
- [`onWillUnmount`](#onwillunmount)
- [`onWillPatch`](#onwillpatch)
- [`onPatched`](#onpatched)
- [`useRef`](#useref)
- [`useSubEnv`](#useSubEnv)
## Overview
Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
Owl hooks serve the same purpose, except that they work for class components
(note: React hooks do not work on class components, and maybe because of that,
there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks).
Hooks works beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component
reactive.
## Example: mouse position
Here is the classical example of a non trivial hook to track the mouse position.
```js
const { useState, onMounted, onWillUnmount } = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener("mousemove", update);
});
onWillUnmount(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// Main root component
class App extends owl.Component {
static template = xml`
<div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
```
Note that we use the prefix `use` for hooks, just like in React. This is just
a convention.
## Example: autofocus
Hooks can be combined to create the desired effect. For example, the following
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
to create an easy way to focus an input whenever it appears in the DOM:
```js
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
```
This hook takes the name of a valid `t-ref` directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
```js
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
constructor(...args) {
super(...args);
useAutofocus("myinput");
}
}
```
## Reference
### One rule
There is only one rule: every hook for a component have to be called in the
constructor (or in class fields):
```js
// ok
class SomeComponent extends Component {
state = useState({ value: 0 });
}
// also ok
class SomeComponent extends Component {
constructor(...args) {
super(...args);
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({ value: 0 });
}
}
```
### `useState`
The `useState` hook is certainly the most important hooks for Owl components:
this is what enables component to be reactive, to react to state change.
The `useState` hook has to be given an object or an array, and will return
an observed version of it (using a `Proxy`).
```javascript
const { useState } = owl.hooks;
class Counter extends owl.Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
```
### `onMounted`
`onMounted` is not an user hook, but is a building block designed to help make useful
abstractions. `onMounted` registers a callback, which will be called when the component
is mounted (see example on top of this page).
### `onWillUnmount`
`onWillUnmount` is not an user hook, but is a building block designed to help make useful
abstractions. `onWillUnmount` registers a callback, which will be called when the component
is unmounted (see example on top of this page).
### `onWillPatch`
`onWillPatch` is not an user hook, but is a building block designed to help make useful
abstractions. `onWillPatch` registers a callback, which will be called just
before the component patched.
### `onPatched`
`onPatched` is not an user hook, but is a building block designed to help make useful
abstractions. `onPatched` registers a callback, which will be called just
after the component patched.
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive:
```xml
<div>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
</div>
```
In this example, the component will be able to access the `div` and the component
`SubComponent` using the `useRef` hook:
```js
class Parent extends Component {
subRef = useRef("someComponent");
divRef = useRef("someDiv");
someMethod() {
// here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component
}
}
```
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
```
Here, the references needs to be set like this:
```js
this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
### `useSubEnv`
The environment is sometimes useful to share some common information between
all components. But sometimes, we want to *scope* that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
`model` object available to all sub component, but not to the whole application.
This is where the `useSubEnv` hook may be useful: it let a component add some
information to the environment in a way that only the component and its children
can access it:
```js
class FormComponent extends Component {
constructor(...args) {
super(...args);
const model = makeModel();
useSubEnv({ model });
}
}
```
The `useSubEnv` takes one argument: an object which contains some key/value that
will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be
affected.
+3 -33
View File
@@ -11,40 +11,10 @@ For example, this code will display `update` in the console:
```javascript
const observer = new owl.Observer();
observer.notifyCB = () => console.log("update");
observer.observe(obj);
const obj = observer.observe({ a: { b: 1 } });
const obj = { a: { b: 1 } };
obj.a.b = 2;
```
## Technical Limitations
Since the observer uses getters and setters, it is actually unable to react to
changes in three situations:
- adding a key to an object
- deleting a key from an object
- modifying an array by setting a new value at a given index
In those situations, we need a way to tell the observer that something happened.
This can be done by using the `set` and `delete` (only for objects) static
methods of the `Observer`.
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // won't notify the change
owl.Observer.set(obj, "b", 2); // will notify the change
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
```javascript
const observer = new owl.Observer();
const arr = ["a"];
observer.observe(arr);
arr[0] = "b"; // won't notify the change
owl.Observer.set(arr, 0, "b"); // will notify the change
```
The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers.
+3 -1
View File
@@ -72,11 +72,13 @@ Here are a few steps that we may take to get started:
Let us now add the javascript to make it work, in `app.js`:
```javascript
const useState = owl.hooks.useState;
class ClickCounter extends owl.Component {
static template = "clickcounter";
constructor() {
super(...arguments);
this.state = { value: 0 };
this.state = useState({ value: 0 });
}
increment() {
-1
View File
@@ -72,7 +72,6 @@ needs. Here is a list of all Owl specific directives:
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
| `t-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
+12
View File
@@ -9,9 +9,18 @@ build applications. Here is a complete representation of its content:
owl
Component
QWeb
useState
core
EventBus
Observer
hooks
onMounted
onWillUnmount
onWillPatch
onPatched
useState
useRef
useSubEnv
router
Link
RouteComponent
@@ -29,11 +38,14 @@ owl
whenReady
```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference
- [Animations](animations.md)
- [Component](component.md)
- [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Observer](observer.md)
- [QWeb](qweb.md)
- [Router](router.md)
+29
View File
@@ -6,6 +6,7 @@
- [Development Mode](#development-mode)
- [Playground](#playground)
- [Benchmarks](#benchmarks)
- [Single File Component](#single-file-component)
## Overview
@@ -56,3 +57,31 @@ Note: This is more an internal tool, useful for people working on Owl.
The benchmarks application is a very small application, implemented in different
frameworks, and in different versions of Owl. This is a simple internal tool,
useful to compare various performance metrics on some tasks.
## Single File Component
If you want to have `xml` syntax highlighting while using the `xml` helper which
helps you define inline templates, there is a VS Code addon `Comment tagged template`
which, if installed, does exactly that. To enable it, you need to add a comment,
like this:
```js
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main two-columns">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class MyComponent extends Component {
static template = TEMPLATE;
static components = { Sidebar, Content };
// rest of component...
}
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "owl-framework",
"version": "0.21.0",
"version": "0.22.0",
"description": "Odoo Web Library (OWL)",
"main": "src/index.ts",
"scripts": {
+128 -124
View File
@@ -11,7 +11,8 @@ import "./props_validation";
* contains:
*
* - the Env interface (generic type for the environment)
* - the Meta interface (the owl specific metadata attached to a component)
* - the Fiber interface (owl metadata attached to a rendering)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class
*/
@@ -33,6 +34,27 @@ export interface Env {
[key: string]: any;
}
/**
* Fibers are small abstractions designed to contain all the internal state
* associated to a "rendering work unit", relative to a specific component.
*
* A rendering will cause the creation of a fiber for each impacted components.
*/
export interface Fiber<Props> {
force: boolean;
rootFiber: Fiber<any> | null;
isCancelled: boolean;
scope: any;
vars: any;
patchQueue: Fiber<any>[];
component: Component<any, any>;
vnode: VNode | null;
props: Props;
promise: Promise<VNode> | null;
// handlers?: any;
// mountedHandlers?: any;
}
/**
* This is mostly an internal detail of implementation. The Meta interface is
* useful to typecheck and describe the internal keys used by Owl to manage the
@@ -48,26 +70,24 @@ interface Internal<T extends Env, Props> {
// parent and children keys are obviously useful to setup the parent-children
// relationship.
parent: Component<T, any, any> | null;
children: { [key: number]: Component<T, any, any> };
parent: Component<T, any> | null;
children: { [key: number]: Component<T, any> };
// children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered.
cmap: { [key: number]: number };
renderId: number;
// the renderProps and renderPromise keys are only useful for the "prepare"
// step of the lifecycle of a component. Once a component has been rendered
// and patched, it is no longer useful.
renderProps: Props | null;
renderPromise: Promise<VNode> | null;
currentFiber: Fiber<Props> | null;
boundHandlers: { [key: number]: any };
observer: Observer | null;
render: CompiledTemplate | null;
mountedHandlers: { [key: number]: Function };
mountedCB: Function | null;
willUnmountCB: Function | null;
willPatchCB: Function | null;
patchedCB: Function | null;
classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
}
//------------------------------------------------------------------------------
@@ -75,10 +95,14 @@ interface Internal<T extends Env, Props> {
//------------------------------------------------------------------------------
let nextId = 1;
export class Component<T extends Env, Props extends {}, State extends {}> {
export class Component<T extends Env, Props extends {}> {
readonly __owl__: Internal<Env, Props>;
static template?: string | null = null;
static _template?: string | null = null;
static _current?: any | null = null;
static components = {};
static props?: any;
static defaultProps?: any;
/**
* The `el` is the root element of the component. Note that it could be null:
@@ -87,20 +111,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
}
static components = {};
env: T;
state?: State;
props: Props;
// type of props is not easily representable in typescript...
static props?: any;
static defaultProps?: any;
refs: {
[key: string]: Component<T, any, any> | HTMLElement | undefined;
} = {};
//--------------------------------------------------------------------------
// Lifecycle
//--------------------------------------------------------------------------
@@ -124,8 +138,9 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* hand. Other components should be created automatically by the framework (with
* the t-component directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps;
Component._current = this;
if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps);
}
@@ -135,7 +150,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{};
let id: number = nextId++;
let p: Component<T, any, any> | null = null;
let p: Component<T, any> | null = null;
if (parent instanceof Component) {
p = parent;
this.env = parent.env;
@@ -169,14 +184,16 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
parent: p,
children: {},
cmap: {},
renderId: 1,
renderPromise: null,
renderProps: props || null,
currentFiber: null,
boundHandlers: {},
mountedHandlers: {},
mountedCB: null,
willUnmountCB: null,
willPatchCB: null,
patchedCB: null,
observer: null,
render: null,
classObj: null
classObj: null,
refs: null
};
}
@@ -223,8 +240,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* It is not called on the initial render. This is useful to get some
* information which are in the DOM. For example, the current position of the
* scrollbar
*
* The return value of willPatch will be given to the patched function.
*/
willPatch(): any {}
@@ -240,10 +255,8 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* One need to be careful, because updates here will cause rerender, which in
* turn will cause other calls to updated. So, we need to be particularly
* careful at avoiding endless cycles.
*
* The snapshot parameter is the result of the call to willPatch.
*/
patched(snapshot: any) {}
patched() {}
/**
* willUnmount is a hook that is called each time just before a component is
@@ -279,17 +292,20 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (__owl__.isMounted) {
return;
}
const fiber = this.__createFiber(false, undefined, undefined, undefined);
if (!__owl__.vnode) {
const vnode = await this.__prepare();
fiber.promise = this.__prepareAndRender(fiber);
const vnode = await fiber.promise;
if (__owl__.isDestroyed) {
// component was destroyed before we get here...
return;
}
this.__patch(vnode);
} else if (renderBeforeRemount) {
const patchQueue = [];
await this.__render(false, patchQueue, undefined, undefined);
this.__applyPatchQueue(<any[]>patchQueue);
fiber.patchQueue.push(fiber);
fiber.promise = this.__render(fiber);
await fiber.promise;
this.__applyPatchQueue(fiber);
}
target.appendChild(this.el!);
@@ -323,15 +339,15 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
if (!__owl__.isMounted) {
return;
}
const patchQueue = [];
const fiber = this.__createFiber(force, undefined, undefined, undefined);
fiber.patchQueue.push(fiber);
fiber.promise = this.__render(fiber);
await fiber.promise;
const renderId = ++__owl__.renderId;
await this.__render(force, patchQueue, undefined, undefined);
if (__owl__.isMounted && renderId === __owl__.renderId) {
if (__owl__.isMounted && fiber === __owl__.currentFiber) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
this.__applyPatchQueue(<any[]>patchQueue);
this.__applyPatchQueue(fiber);
}
}
@@ -364,26 +380,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
return true;
}
/**
* This method is the correct way to update the environment of a component. Doing
* this will cause a full rerender of the component and its children, so this is
* an operation that should not be done frequently.
*
* A good usecase for updating the environment would be to update some mostly
* static config keys, such as a boolean to determine if we are in mobile
* mode or not.
*/
async updateEnv(nextEnv: Partial<T>): Promise<void> {
const __owl__ = this.__owl__;
if (__owl__.parent && __owl__.parent.env === this.env) {
this.env = Object.create(this.env);
}
Object.assign(this.env, nextEnv);
if (__owl__.isMounted) {
await this.render(true);
}
}
/**
* Emit a custom event of type 'eventType' with the given 'payload' on the
* component's el, if it exists. However, note that the event will only bubble
@@ -405,6 +401,27 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
// Private
//--------------------------------------------------------------------------
/**
* This method is a helper to create a fiber element.
*/
__createFiber(force, scope, vars, parent?: Fiber<any>): Fiber<Props> {
const fiber: Fiber<Props> = {
force,
scope,
vars,
rootFiber: null,
isCancelled: false,
component: this,
vnode: null,
patchQueue: parent ? parent.patchQueue : [],
props: this.props,
promise: null
};
fiber.rootFiber = parent ? parent.rootFiber : fiber;
this.__owl__.currentFiber = fiber;
return fiber;
}
/**
* Private helper to perform a full destroy, from the point of view of an Owl
* component. It does not remove the el (this is done only once on the top
@@ -416,7 +433,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times.
*/
__destroy(parent: Component<any, any, any> | null) {
__destroy(parent: Component<any, any> | null) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
@@ -446,20 +463,22 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
__owl__.isMounted = true;
const handlers = __owl__.mountedHandlers;
for (let key in handlers) {
handlers[key]();
}
try {
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB()
}
} catch (e) {
errorHandler(e, this);
}
}
__callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
if (__owl__.willUnmountCB) {
__owl__.willUnmountCB();
}
this.willUnmount();
__owl__.isMounted = false;
const children = __owl__.children;
for (let id in children) {
@@ -476,12 +495,11 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/
async __updateProps(
nextProps: Props,
forceUpdate: boolean = false,
patchQueue?: any[],
parentFiber: Fiber<any>,
scope?: any,
vars?: any
): Promise<void> {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) {
@@ -489,7 +507,10 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.__render(forceUpdate, patchQueue, scope, vars);
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.patchQueue.push(fiber);
await this.__render(fiber);
}
}
@@ -503,14 +524,18 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
__owl__.vnode = patch(target, vnode);
}
__prepare(scope?: Object, vars?: any): Promise<VNode> {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
__owl__.renderPromise = this.__prepareAndRender(scope, vars);
return __owl__.renderPromise;
/**
* The __prepare method is only called by the t-component directive, when a
* subcomponent is created. It gets its scope and vars, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber<any>, scope: any, vars: any): Promise<VNode> {
const fiber = this.__createFiber(parentFiber.force, scope, vars, parentFiber);
fiber.promise = this.__prepareAndRender(fiber);
return fiber.promise;
}
async __prepareAndRender(scope?: Object, vars?: any): Promise<VNode> {
async __prepareAndRender(fiber: Fiber<Props>): Promise<VNode> {
try {
await this.willStart();
} catch (e) {
@@ -544,21 +569,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
__owl__.render = qweb.render.bind(qweb, p._template);
this.__observeState();
return this.__render(false, [], scope, vars);
return this.__render(fiber);
}
__render(
force: boolean = false,
patchQueue: any[] = [],
scope?: Object,
vars?: any
): Promise<VNode> {
__render(fiber: Fiber<Props>): Promise<VNode> {
const __owl__ = this.__owl__;
const promises: Promise<void>[] = [];
const patch: any[] = [this];
patchQueue.push(patch);
if (__owl__.observer) {
__owl__.observer.allowMutations = false;
}
@@ -567,17 +583,13 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
vnode = __owl__.render!(this, {
promises,
handlers: __owl__.boundHandlers,
mountedHandlers: __owl__.mountedHandlers,
forceUpdate: force,
patchQueue,
scope,
vars
fiber: fiber
});
} catch (e) {
vnode = __owl__.vnode || h("div");
errorHandler(e, this);
}
patch.push(vnode);
fiber.vnode = vnode;
if (__owl__.observer) {
__owl__.observer.allowMutations = true;
}
@@ -625,19 +637,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
}
/**
* Enable the observe feature on the state. We only create an observer if
* there is some state to be observed.
*/
__observeState() {
if (this.state) {
const __owl__ = this.__owl__;
__owl__.observer = new Observer();
this.state = __owl__.observer.observe(this.state);
__owl__.observer.notifyCB = this.render.bind(this);
}
}
/**
* Apply default props (only top level).
*
@@ -655,29 +654,34 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
}
/**
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode.
* Apply the given patch queue from a fiber.
* 1) Call 'willPatch' on the component of each patch
* 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order
* 3) Call 'patched' on the component of each patch, in reverse order
*/
__applyPatchQueue(patchQueue: any[]) {
let component = this;
__applyPatchQueue(fiber: Fiber<Props>) {
const patchQueue = fiber.patchQueue;
let component: Component<any, any> = this;
try {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
component = patch[0];
patch.push(patch[0].willPatch());
component = patchQueue[i].component;
if (component.__owl__.willPatchCB) {
component.__owl__.willPatchCB();
}
component.willPatch();
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
patch[0].__patch(patch[1]);
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
component = patch[0];
patch[0].patched(patch[2]);
component = patchQueue[i].component;
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
}
} catch (e) {
errorHandler(e, component);
@@ -697,7 +701,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
* If there are no such component, we destroy everything. This is better than
* being in a corrupted state.
*/
function errorHandler(error: Error, component: Component<any, any, any>) {
function errorHandler(error: Error, component: Component<any, any>) {
let canCatch = false;
let qweb = component.env.qweb;
let root = component;
+33 -18
View File
@@ -1,4 +1,5 @@
import { QWeb } from "../qweb/index";
import { INTERP_REGEXP } from "../qweb/context";
import { MODS_CODE } from "../qweb/extensions";
//------------------------------------------------------------------------------
@@ -262,9 +263,10 @@ QWeb.addDirective({
let refExpr = "";
let refKey: string = "";
if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${componentID};`;
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
}
let transitionsInsertCode = "";
if (transition) {
@@ -272,7 +274,7 @@ QWeb.addDirective({
}
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) {
finalizeComponentCode += `delete context.refs[${refKey}];`;
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
}
if (transition) {
finalizeComponentCode = `let finalize = () => {
@@ -356,8 +358,12 @@ QWeb.addDirective({
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
}
let shouldProxy = false;
if (async || keepAlive) {
ctx.addLine(
`const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});`
);
}
if (async) {
ctx.addLine(`const patchQueue${componentID} = [];`);
ctx.addLine(
`c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);`
);
@@ -380,10 +386,12 @@ QWeb.addDirective({
ctx.addLine(`let props${componentID} = {${propStr}};`);
}
ctx.addIf(
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
);
ctx.addIf(`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.renderProps)`);
ctx.addLine(`def${defID} = w${componentID}.__owl__.renderPromise;`);
ctx.addIf(
`utils.shallowEqual(props${componentID}, w${componentID}.__owl__.currentFiber.props)`
);
ctx.addLine(`def${defID} = w${componentID}.__owl__.currentFiber.promise;`);
ctx.addElse();
ctx.addLine(`w${componentID}.destroy();`);
ctx.addLine(`w${componentID} = false;`);
@@ -392,10 +400,16 @@ QWeb.addDirective({
ctx.addIf(`!w${componentID}`);
// new component
ctx.addLine(`let componentKey${componentID} = ${ctx.interpolate(value)};`);
let dynamicFallback = "";
if (!value.match(INTERP_REGEXP)) {
dynamicFallback = `|| ${ctx.formatExpression(value)}`;
}
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
ctx.addLine(
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
`let W${componentID} = context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}]${dynamicFallback};`
);
// maybe only do this in dev mode...
ctx.addLine(
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
@@ -441,14 +455,15 @@ QWeb.addDirective({
}
}
let scopeVars = "";
let scopeVars;
if (hasSlots) {
scopeVars += ctx.scopeVars.length ? `Object.assign({}, scope)` : varDefs.length ? `{}` : "";
if (varDefs.length) {
scopeVars += `, {${varDefs.join(",")}}`;
}
let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
scopeVars = `${scope}, ${vars}`;
} else {
scopeVars = "undefined, undefined";
}
ctx.addLine(`def${defID} = w${componentID}.__prepare(${scopeVars});`);
ctx.addLine(`def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
let registerCode = `c${ctx.parentNode}[_${dummyID}_index]=pvnode;`;
if (shouldProxy) {
@@ -460,19 +475,19 @@ QWeb.addDirective({
ctx.addElse();
// need to update component
let patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber";
if (keepAlive) {
// if we have t-keepalive="1", the component could be unmounted, but then
// we __updateProps is called. This is ok, but we do not want to call
// the willPatch/patched hooks of the component in this case, so we
// disable the patch queue
patchQueueCode = `w${componentID}.__owl__.isMounted ? ${patchQueueCode} : []`;
patchQueueCode = `w${componentID}.__owl__.isMounted ? extra.fiber : fiber${componentID}`;
}
if (QWeb.dev) {
ctx.addLine(`utils.validateProps(w${componentID}.constructor, props${componentID})`);
}
ctx.addLine(
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode}${scopeVars &&
`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, ${patchQueueCode}${scopeVars &&
", " + scopeVars});`
);
let keepAliveCode = "";
@@ -492,7 +507,7 @@ QWeb.addDirective({
if (async) {
ctx.addLine(
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));`
);
} else {
ctx.addLine(`extra.promises.push(def${defID});`);
+1 -1
View File
@@ -33,7 +33,7 @@ export class Observer {
}
}
observe(value: any, parent?: any): any {
observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object'
return value;
+104
View File
@@ -0,0 +1,104 @@
import { Component } from "./component/component";
import { Observer } from "./core/observer";
/**
* Owl Hook System
*
* This file introduces the concept of hooks, similar to React or Vue hooks.
* We have currently an implementation of:
* - useState (reactive state)
* - onMounted
* - onWillUnmount
* - useRef
*/
// -----------------------------------------------------------------------------
// useState
// -----------------------------------------------------------------------------
/**
* This is the main way a component can be made reactive. The useState hook
* will return an observed object (or array). Changes to that value will then
* trigger a rerendering of the current component.
*/
export function useState<T>(state: T): T {
const component: Component<any, any> = Component._current;
const __owl__ = component.__owl__;
if (!__owl__.observer) {
__owl__.observer = new Observer();
__owl__.observer.notifyCB = component.render.bind(component);
}
return __owl__.observer.observe(state);
}
// -----------------------------------------------------------------------------
// Life cycle hooks
// -----------------------------------------------------------------------------
function makeLifecycleHook(method: string, reverse: boolean = false) {
return function(cb) {
const component: Component<any, any> = Component._current;
if (component.__owl__[method]) {
const current = component.__owl__[method];
if (reverse) {
component.__owl__[method] = function() {
current.call(component);
cb.call(component);
};
} else {
component.__owl__[method] = function() {
cb.call(component);
current.call(component);
};
}
} else {
component.__owl__[method] = cb;
}
};
}
export const onMounted = makeLifecycleHook("mountedCB", true);
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
export const onWillPatch = makeLifecycleHook("willPatchCB");
export const onPatched = makeLifecycleHook("patchedCB", true);
// -----------------------------------------------------------------------------
// useRef
// -----------------------------------------------------------------------------
/**
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
interface Ref {
el: HTMLElement | null;
comp: Component<any, any> | null;
}
export function useRef(name: string): Ref {
const __owl__ = Component._current.__owl__;
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null;
},
get comp(): Component<any, any> | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null;
}
};
}
// -----------------------------------------------------------------------------
// useSubEnv
// -----------------------------------------------------------------------------
/**
* This hook is a simple way to let components use a sub environment. Note that
* like for all hooks, it is important that this is only called in the
* constructor method.
*/
export function useSubEnv(nextEnv) {
const component = Component._current;
component.env = Object.assign(Object.create(component.env), nextEnv);
}
+4
View File
@@ -11,17 +11,21 @@ import { ConnectedComponent } from "./store/connected_component";
import { Store } from "./store/store";
import * as _utils from "./utils";
import * as _tags from "./tags";
import * as _hooks from "./hooks";
import { Link } from "./router/Link";
import { RouteComponent } from "./router/RouteComponent";
import { Router } from "./router/Router";
export { Component } from "./component/component";
export { QWeb };
export const useState = _hooks.useState;
export const core = { EventBus, Observer };
export const router = { Router, RouteComponent, Link };
export const store = { Store, ConnectedComponent };
export const utils = _utils;
export const tags = _tags;
export const hooks = _hooks;
export const __info__ = {};
Object.defineProperty(__info__, "mode", {
+1 -1
View File
@@ -244,7 +244,7 @@ QWeb.addDirective({
varCode = `{${content}}`;
}
ctx.addLine(
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, vars: ${varCode}, scope}));`
`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`
);
return true;
}
+6 -1
View File
@@ -1,5 +1,6 @@
import { compileExpr, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
@@ -19,6 +20,7 @@ export class Context {
shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false;
@@ -61,6 +63,9 @@ export class Context {
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it.
@@ -163,7 +168,7 @@ export class Context {
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
*/
interpolate(s: string): string {
let matches = s.match(/\{\{.*?\}\}/g);
let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) {
return `(${this.formatExpression(s.slice(2, -2))})`;
}
+3 -38
View File
@@ -79,9 +79,10 @@ QWeb.addDirective({
name: "ref",
priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`);
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
}
});
@@ -182,42 +183,6 @@ QWeb.addDirective({
}
});
//------------------------------------------------------------------------------
// t-mounted
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "mounted",
priority: 97,
atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) {
ctx.rootContext.shouldDefineOwner = true;
const eventName = fullName.slice(5);
if (!eventName) {
throw new Error("Missing event name with t-on directive");
}
let extraArgs;
let handler = value.replace(/\(.*\)/, function(args) {
extraArgs = args.slice(1, -1);
return "";
});
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs
)});`
);
} else {
ctx.addLine(
`extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
);
}
addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`);
}
});
//------------------------------------------------------------------------------
// t-slot
//------------------------------------------------------------------------------
@@ -254,7 +219,7 @@ QWeb.addDirective({
const type = node.getAttribute("type");
let handler;
let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(`state.${value}`);
const expr = ctx.formatExpression(value);
if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`);
+26 -8
View File
@@ -2,6 +2,7 @@ import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { Context } from "./context";
import { shallowEqual } from "../utils";
import { addNS } from "../vdom/vdom";
/**
* Owl QWeb Engine
@@ -93,11 +94,17 @@ const UTILS: Utils = {
}
return expr;
},
shallowEqual
shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
}
};
function parseXML(xml: string): Document {
const parser = new DOMParser();
// we remove comments from the xml string
xml = xml.replace(/<!--[\s\S]*?-->/g, "");
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
@@ -241,7 +248,7 @@ export class QWeb extends EventBus {
this._processTemplate(elem);
const template = {
elem,
fn: (context, extra) => {
fn: function(this: QWeb, context, extra) {
const compiledFunction = this._compile(name, elem);
template.fn = compiledFunction;
return compiledFunction.call(this, context, extra);
@@ -309,8 +316,8 @@ export class QWeb extends EventBus {
* to render a full component tree, since this is an asynchronous operation.
* This method can only render templates without components.
*/
renderToString(name: string, context: EvalContext = {}): string {
const vnode = this.render(name, context);
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
const vnode = this.render(name, context, extra);
if (vnode.sel === undefined) {
return vnode.text!;
}
@@ -344,8 +351,8 @@ export class QWeb extends EventBus {
if (parentContext) {
ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables);
ctx.nextID = parentContext.parentNode! + 1;
ctx.parentNode = parentContext.parentNode!;
ctx.nextID = parentContext.nextID + 1;
ctx.parentNode = parentContext.parentNode || ctx.nextID++;
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
@@ -354,12 +361,12 @@ export class QWeb extends EventBus {
for (let v in parentContext.variables) {
let variable = <any>parentContext.variables[v];
if (variable.id) {
ctx.addLine(`let ${variable.id} = extra.vars.${variable.id}`);
ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
}
}
}
if (parentContext) {
ctx.addLine(" Object.assign(context, extra.scope);");
ctx.addLine(" Object.assign(context, extra.fiber.scope);");
}
this._compileNode(elem, ctx);
@@ -534,6 +541,17 @@ export class QWeb extends EventBus {
}
this._compileChildren(node, ctx);
// svg support
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
// the root node. This is the easiest way to support svg sub components:
// they need to have a g tag as root. Otherwise, we would need a complete
// list of allowed svg tags.
const shouldAddNS =
node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
if (shouldAddNS) {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
}
for (let { directive, value, fullName } of validDirectives) {
if (directive.finalize) {
+1 -1
View File
@@ -4,7 +4,7 @@ import { Destination, RouterEnv } from "./Router";
type Props = Destination;
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
static template = xml`
<a t-att-class="{'router-link-active': isActive }"
t-att-href="href"
+9 -18
View File
@@ -1,27 +1,18 @@
import { Component } from "../component/component";
import { xml } from "../tags";
export class RouteComponent extends Component<any, {}, {}> {
export class RouteComponent extends Component<any, {}> {
static template = xml`
<t t-foreach="routes" t-as="route">
<t t-if="env.router.currentRouteName === route.name">
<t t-component="{{route.component}}" t-props="env.router.currentParams"/>
</t>
<t>
<t
t-if="routeComponent"
t-component="routeComponent"
t-key="env.router.currentRouteName"
t-props="env.router.currentParams" />
</t>
`;
routes: any[] = [];
constructor(parent, props) {
super(parent, props);
const router = this.env.router;
for (let name of router.routeIds) {
const route = router.routes[name];
if (route.component) {
this.routes.push({
name: route.name,
component: "__component__" + route.name
});
}
}
get routeComponent(): any {
return this.env.router.currentRoute && this.env.router.currentRoute.component;
}
}
+7 -9
View File
@@ -1,4 +1,5 @@
import { Component, Env } from "../component/component";
import { Component, Env, Fiber } from "../component/component";
import { VNode } from "../vdom/index";
//------------------------------------------------------------------------------
// Connect function
@@ -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 {
View File
+3 -4
View File
@@ -10,8 +10,6 @@ import { QWeb } from "./qweb/index";
* The plan is to add a few other tags such as css, globalcss.
*/
/**
* XML tag helper for defining templates. With this, one can simply define
* an inline template with just the template xml:
@@ -21,8 +19,9 @@ import { QWeb } from "./qweb/index";
* }
* ```
*/
export function xml(strings) {
export function xml(strings, ...args) {
const name = `__template__${QWeb.nextId++}`;
QWeb.registerTemplate(name, strings[0]);
const value = String.raw(strings, ...args);
QWeb.registerTemplate(name, value);
return name;
}
+1 -3
View File
@@ -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
View File
@@ -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);
}
+12 -12
View File
@@ -17,9 +17,9 @@ exports[`animations t-transition combined with component 1`] = `
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
@@ -27,17 +27,17 @@ exports[`animations t-transition combined with component 1`] = `
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
@@ -63,9 +63,9 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let _2_index = c1.length;
c1.push(null);
let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
@@ -73,17 +73,17 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
w4.destroy();
};
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
+11 -36
View File
@@ -1,10 +1,10 @@
import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index";
import { useState, useRef } from "../src/hooks";
import {
makeDeferred,
makeTestFixture,
makeTestEnv,
nextTick,
patchNextFrame,
renderToDOM,
unpatchNextFrame
@@ -36,7 +36,7 @@ afterEach(() => {
fixture.remove();
});
class Widget extends Component<any, any, any> {}
class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------
// Tests
@@ -106,7 +106,7 @@ describe("animations", () => {
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { hide: false };
state = useState({ hide: false });
}
const widget = new TestWidget(env);
@@ -148,7 +148,8 @@ describe("animations", () => {
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { hide: false };
state = useState({ hide: false });
span = useRef("span");
}
const widget = new TestWidget(env);
@@ -163,7 +164,7 @@ describe("animations", () => {
});
await widget.mount(fixture);
spanNode = widget.el!.children[0];
expect(widget.refs.span).toBe(spanNode);
expect(widget.span.el).toBe(spanNode);
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
@@ -217,7 +218,7 @@ describe("animations", () => {
class Child extends Widget {}
class Parent extends Widget {
static components = { Child: Child };
state = { display: true };
state = useState({ display: true });
}
const widget = new Parent(env);
@@ -263,28 +264,6 @@ describe("animations", () => {
expect(fixture.innerHTML).toBe("<div></div>");
});
test("t-transition combined with t-mounted", async () => {
env.qweb.addTemplate(
"TestWidget",
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
);
class TestWidget extends Widget {
state = { flag: false };
f() {}
}
const widget = new TestWidget(env);
widget.f = jest.fn();
await widget.mount(fixture);
patchNextFrame(cb => cb());
expect(widget.f).toHaveBeenCalledTimes(0);
widget.state.flag = true;
await nextTick();
expect(widget.f).toHaveBeenCalledTimes(1);
unpatchNextFrame();
});
test("t-transition, remove and re-add before transitionend", async () => {
expect.assertions(11);
@@ -297,10 +276,8 @@ describe("animations", () => {
</templates>`
);
class Parent extends Widget {
constructor(parent) {
super(parent);
this.state = { flag: false };
}
state = useState({ flag: false });
toggle() {
this.state.flag = !this.state.flag;
}
@@ -357,10 +334,8 @@ describe("animations", () => {
class Child extends Widget {}
class Parent extends Widget {
static components = { Child };
constructor(parent) {
super(parent);
this.state = { flag: false };
}
state = useState({ flag: false });
toggle() {
this.state.flag = !this.state.flag;
}
File diff suppressed because it is too large Load Diff
@@ -21,9 +21,9 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let _2_index = c1.length;
c1.push(null);
let props4 = {message:1};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
def3 = w4.__owl__.renderPromise;
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
if (utils.shallowEqual(props4, w4.__owl__.currentFiber.props)) {
def3 = w4.__owl__.currentFiber.promise;
} else {
w4.destroy();
w4 = false;
@@ -31,16 +31,16 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
}
if (!w4) {
let componentKey4 = \`Child\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| context['Child'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
utils.validateProps(W4, props4)
w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare();
def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
utils.validateProps(w4.constructor, props4)
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
}
extra.promises.push(def3);
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -22,7 +22,7 @@ afterEach(() => {
QWeb.dev = dev;
});
class Widget extends Component<any, any, any> {}
class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------
// Tests
@@ -334,7 +334,8 @@ describe("default props", () => {
const w = new TestWidget(env, { p: 1 });
await w.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
await w.__updateProps({});
const fiber = w.__createFiber(false, undefined, undefined, undefined);
await w.__updateProps({}, fiber);
await w.render();
expect(w.props.p).toBe(4);
expect(fixture.innerHTML).toMatchSnapshot();
+8 -3
View File
@@ -99,10 +99,15 @@ export function renderToDOM(
* Note that the result of renderToString is guaranteed to be the same as the
* one from QWeb.
*/
export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}): string {
const node = renderToDOM(qweb, t, context);
export function renderToString(
qweb: QWeb,
t: string,
context: EvalContext = {},
extra?: any
): string {
const node = renderToDOM(qweb, t, context, extra);
const result = node instanceof Text ? node.textContent! : node.outerHTML;
if (result !== qweb.renderToString(t, context)) {
if (result !== qweb.renderToString(t, context, extra)) {
throw new Error("HTML string returned by renderToString helper does not match QWeb render");
}
return result;
+359
View File
@@ -0,0 +1,359 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component";
import {
useState,
onMounted,
onWillUnmount,
useRef,
onPatched,
onWillPatch,
useSubEnv
} from "../src/hooks";
import { xml } from "../src/tags";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("hooks", () => {
test("can use a state hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 });
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div>42</div>");
counter.counter.value = 3;
await nextTick();
expect(fixture.innerHTML).toBe("<div>3</div>");
});
test("can use onMounted, onWillUnmount", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("mounted");
});
onWillUnmount(() => {
steps.push("willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount");
expect(fixture.innerHTML).toBe("<div>hey</div>");
expect(steps).toEqual(["mounted"]);
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["mounted", "willunmount"]);
});
test("mounted, willUnmount, onMounted, onWillUnmount order", async () => {
const steps: string[] = [];
function useMyHook() {
onMounted(() => {
steps.push("hook:mounted");
});
onWillUnmount(() => {
steps.push("hook:willunmount");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook();
}
mounted() {
steps.push("comp:mounted");
}
willUnmount() {
steps.push("comp:willunmount");
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["comp:mounted", "hook:mounted", "hook:willunmount", "comp:willunmount"]);
});
test("two different call to mounted/willunmount should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
onMounted(() => {
steps.push("hook:mounted" + i);
});
onWillUnmount(() => {
steps.push("hook:willunmount" + i);
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey</div>`;
constructor(env) {
super(env);
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual([
"hook:mounted1",
"hook:mounted2",
"hook:willunmount2",
"hook:willunmount1"
]);
});
test("useRef hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
button = useRef("button");
value = 0;
increment() {
this.value++;
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
}
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
counter.increment();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
});
test("can use onPatched, onWillPatch", async () => {
const steps: string[] = [];
function useMyHook() {
onWillPatch(() => {
steps.push("willPatch");
});
onPatched(() => {
steps.push("patched");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
useMyHook();
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch");
expect(steps).toEqual([]);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["willPatch", "patched"]);
});
test("patched, willPatch, onPatched, onWillPatch order", async () => {
const steps: string[] = [];
function useMyHook() {
onPatched(() => {
steps.push("hook:patched");
});
onWillPatch(() => {
steps.push("hook:willPatch");
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div><t t-if="state.flag">hey</t></div>`;
state = useState({ flag: true });
constructor(env) {
super(env);
useMyHook();
}
willPatch() {
steps.push("comp:willPatch");
}
patched() {
steps.push("comp:patched");
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
component.state.flag = false;
await nextTick();
expect(steps).toEqual(["hook:willPatch", "comp:willPatch", "comp:patched", "hook:patched"]);
});
test("two different call to willPatch/patched should work", async () => {
const steps: string[] = [];
function useMyHook(i) {
onPatched(() => {
steps.push("hook:patched" + i);
});
onWillPatch(() => {
steps.push("hook:willPatch" + i);
});
}
class MyComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 });
constructor(env) {
super(env);
useMyHook(1);
useMyHook(2);
}
}
const component = new MyComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>hey1</div>");
component.state.value++;
await nextTick();
expect(fixture.innerHTML).toBe("<div>hey2</div>");
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
});
describe("autofocus hook", () => {
function useAutofocus(name) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
test("simple input", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<input t-ref="input1"/>
<input t-ref="input2"/>
</div>`;
constructor(env) {
super(env);
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input><input></div>");
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
test("input in a t-if", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div>
<input t-ref="input1"/>
<t t-if="state.flag"><input t-ref="input2"/></t>
</div>`;
state = useState({ flag: false });
constructor(env) {
super(env);
useAutofocus("input2");
}
}
const component = new SomeComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div><input></div>");
expect(document.activeElement).toBe(document.body);
component.state.flag = true;
await nextTick();
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
});
test("can use sub env", async () => {
class TestComponent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
useSubEnv({ val: 3 });
}
}
const component = new TestComponent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(env).not.toHaveProperty("val");
expect(component.env).toHaveProperty("val");
});
test("parent and child env", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/></div>`;
constructor(env) {
super(env);
useSubEnv({ val: 5 });
}
}
class Parent extends Component<any, any> {
static template = xml`<div><t t-esc="env.val"/><Child/></div>`;
static components = { Child}
constructor(env) {
super(env);
useSubEnv({ val: 3 });
}
}
const component = new Parent(env);
await component.mount(fixture);
expect(fixture.innerHTML).toBe( "<div>3<div>5</div></div>");
});
});
+139 -61
View File
@@ -676,6 +676,54 @@ exports[`misc global 1`] = `
}"
`;
exports[`properly support svg add proper namespace to g tags 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('g', p1, c1);
result = vn1;
var _2 = '50';
var _3 = '50';
var _4 = '4';
var _5 = 'green';
var _6 = '1';
var _7 = 'yellow';
let c8 = [], p8 = {key:8,attrs:{cx: _2,cy: _3,r: _4,stroke: _5,\\"stroke-width\\": _6,fill: _7}};
var vn8 = h('circle', p8, c8);
c1.push(vn8);
c1.push({text: \` \`});
utils.addNameSpace(vn1);
return vn1;
}"
`;
exports[`properly support svg add proper namespace to svg 1`] = `
"function anonymous(context,extra
) {
let utils = this.constructor.utils;
var h = this.h;
var _1 = '100px';
var _2 = '90px';
let c3 = [], p3 = {key:3,attrs:{width: _1,height: _2}};
var vn3 = h('svg', p3, c3);
result = vn3;
var _4 = '50';
var _5 = '50';
var _6 = '4';
var _7 = 'green';
var _8 = '1';
var _9 = 'yellow';
let c10 = [], p10 = {key:10,attrs:{cx: _4,cy: _5,r: _6,stroke: _7,\\"stroke-width\\": _8,fill: _9}};
var vn10 = h('circle', p10, c10);
c3.push(vn10);
c3.push({text: \` \`});
utils.addNameSpace(vn3);
return vn3;
}"
`;
exports[`special cases for some boolean html attributes/properties input type= checkbox, with t-att-checked 1`] = `
"function anonymous(context,extra
) {
@@ -768,6 +816,18 @@ exports[`static templates empty div 1`] = `
}"
`;
exports[`static templates ignore comments 1`] = `
"function anonymous(context,extra
) {
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
c1.push({text: \`hello owl\`});
return vn1;
}"
`;
exports[`static templates simple dynamic value 1`] = `
"function anonymous(context,extra
) {
@@ -872,7 +932,7 @@ exports[`t-call (template calling recursive template, part 1 1`] = `
c1.push(vn2);
c2.push({text: \`hey\`});
if (false) {
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c1, vars: {}, scope}));
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c1, fiber: {vars: {}, scope}}));
}
return vn1;
}"
@@ -884,16 +944,16 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
let owner = context;
var h = this.h;
let c1 = extra.parentNode;
Object.assign(context, extra.scope);
let c2 = [], p2 = {key:2};
var vn2 = h('div', p2, c2);
c1.push(vn2);
let c3 = [], p3 = {key:3};
var vn3 = h('span', p3, c3);
c2.push(vn3);
c3.push({text: \`hey\`});
Object.assign(context, extra.fiber.scope);
let c5 = [], p5 = {key:5};
var vn5 = h('div', p5, c5);
c1.push(vn5);
let c6 = [], p6 = {key:6};
var vn6 = h('span', p6, c6);
c5.push(vn6);
c6.push({text: \`hey\`});
if (false) {
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c2, vars: {}, scope}));
this.recursiveFns['__3'].call(this, context, Object.assign({}, extra, {parentNode: c5, fiber: {vars: {}, scope}}));
}
}"
`;
@@ -939,7 +999,7 @@ exports[`t-call (template calling recursive template, part 2 1`] = `
scope.subtree = context.subtree;
context.subtree_value = _8[i];
scope.subtree_value = context.subtree_value;
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, vars: {_v0: context['subtree']}, scope}));
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, fiber: {vars: {_v0: context['subtree']}, scope}}));
}
}
return vn1;
@@ -954,38 +1014,38 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
const scope = Object.create(null);
var h = this.h;
let c3 = extra.parentNode;
let _v0 = extra.vars._v0
Object.assign(context, extra.scope);
let c4 = [], p4 = {key:4};
var vn4 = h('div', p4, c4);
c3.push(vn4);
let c5 = [], p5 = {key:5};
var vn5 = h('p', p5, c5);
c4.push(vn5);
var _6 = _v0.val;
if (_6 || _6 === 0) {
c5.push({text: _6});
let _v0 = extra.fiber.vars._v0
Object.assign(context, extra.fiber.scope);
let c12 = [], p12 = {key:12};
var vn12 = h('div', p12, c12);
c3.push(vn12);
let c13 = [], p13 = {key:13};
var vn13 = h('p', p13, c13);
c12.push(vn13);
var _14 = _v0.val;
if (_14 || _14 === 0) {
c13.push({text: _14});
}
var _7 = _v0.children||[];
if (!_7) { throw new Error('QWeb error: Invalid loop expression')}
var _8 = _9 = _7;
if (!(_7 instanceof Array)) {
_8 = Object.keys(_7);
_9 = Object.values(_7);
var _15 = _v0.children||[];
if (!_15) { throw new Error('QWeb error: Invalid loop expression')}
var _16 = _17 = _15;
if (!(_15 instanceof Array)) {
_16 = Object.keys(_15);
_17 = Object.values(_15);
}
var _length8 = _8.length;
for (let i = 0; i < _length8; i++) {
var _length16 = _16.length;
for (let i = 0; i < _length16; i++) {
context.subtree_first = i === 0;
scope.subtree_first = context.subtree_first;
context.subtree_last = i === _length8 - 1;
context.subtree_last = i === _length16 - 1;
scope.subtree_last = context.subtree_last;
context.subtree_index = i;
scope.subtree_index = context.subtree_index;
context.subtree = _8[i];
context.subtree = _16[i];
scope.subtree = context.subtree;
context.subtree_value = _9[i];
context.subtree_value = _17[i];
scope.subtree_value = context.subtree_value;
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c4, vars: {_v0: context['subtree']}, scope}));
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c12, fiber: {vars: {_v0: context['subtree']}, scope}}));
}
}"
`;
@@ -1031,7 +1091,7 @@ exports[`t-call (template calling recursive template, part 3 1`] = `
scope.subtree = context.subtree;
context.subtree_value = _8[i];
scope.subtree_value = context.subtree_value;
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, vars: {_v0: context['subtree']}, scope}));
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c3, fiber: {vars: {_v0: context['subtree']}, scope}}));
}
}
return vn1;
@@ -1046,38 +1106,38 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
const scope = Object.create(null);
var h = this.h;
let c3 = extra.parentNode;
let _v0 = extra.vars._v0
Object.assign(context, extra.scope);
let c4 = [], p4 = {key:4};
var vn4 = h('div', p4, c4);
c3.push(vn4);
let c5 = [], p5 = {key:5};
var vn5 = h('p', p5, c5);
c4.push(vn5);
var _6 = _v0.val;
if (_6 || _6 === 0) {
c5.push({text: _6});
let _v0 = extra.fiber.vars._v0
Object.assign(context, extra.fiber.scope);
let c12 = [], p12 = {key:12};
var vn12 = h('div', p12, c12);
c3.push(vn12);
let c13 = [], p13 = {key:13};
var vn13 = h('p', p13, c13);
c12.push(vn13);
var _14 = _v0.val;
if (_14 || _14 === 0) {
c13.push({text: _14});
}
var _7 = _v0.children||[];
if (!_7) { throw new Error('QWeb error: Invalid loop expression')}
var _8 = _9 = _7;
if (!(_7 instanceof Array)) {
_8 = Object.keys(_7);
_9 = Object.values(_7);
var _15 = _v0.children||[];
if (!_15) { throw new Error('QWeb error: Invalid loop expression')}
var _16 = _17 = _15;
if (!(_15 instanceof Array)) {
_16 = Object.keys(_15);
_17 = Object.values(_15);
}
var _length8 = _8.length;
for (let i = 0; i < _length8; i++) {
var _length16 = _16.length;
for (let i = 0; i < _length16; i++) {
context.subtree_first = i === 0;
scope.subtree_first = context.subtree_first;
context.subtree_last = i === _length8 - 1;
context.subtree_last = i === _length16 - 1;
scope.subtree_last = context.subtree_last;
context.subtree_index = i;
scope.subtree_index = context.subtree_index;
context.subtree = _8[i];
context.subtree = _16[i];
scope.subtree = context.subtree;
context.subtree_value = _9[i];
context.subtree_value = _17[i];
scope.subtree_value = context.subtree_value;
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c4, vars: {_v0: context['subtree']}, scope}));
this.recursiveFns['__10'].call(this, context, Object.assign({}, extra, {parentNode: c12, fiber: {vars: {_v0: context['subtree']}, scope}}));
}
}"
`;
@@ -1118,6 +1178,21 @@ exports[`t-call (template calling t-call with t-if 1`] = `
}"
`;
exports[`t-call (template calling t-call, global templates 1`] = `
"function anonymous(context,extra
) {
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
result = vn1;
let c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`desk\`});
return vn1;
}"
`;
exports[`t-call (template calling with unused body 1`] = `
"function anonymous(context,extra
) {
@@ -1840,6 +1915,7 @@ exports[`t-raw variable 1`] = `
exports[`t-ref can get a dynamic ref on a node 1`] = `
"function anonymous(context,extra
) {
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
@@ -1850,7 +1926,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
const ref3 = \`myspan\${context['id']}\`;
p2.hook = {
create: (_, n) => {
context.refs[ref3] = n.elm;
context.__owl__.refs[ref3] = n.elm;
},
};
return vn1;
@@ -1860,6 +1936,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
exports[`t-ref can get a ref on a node 1`] = `
"function anonymous(context,extra
) {
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h;
let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
@@ -1870,7 +1947,7 @@ exports[`t-ref can get a ref on a node 1`] = `
const ref3 = \`myspan\`;
p2.hook = {
create: (_, n) => {
context.refs[ref3] = n.elm;
context.__owl__.refs[ref3] = n.elm;
},
};
return vn1;
@@ -1880,6 +1957,7 @@ exports[`t-ref can get a ref on a node 1`] = `
exports[`t-ref refs in a loop 1`] = `
"function anonymous(context,extra
) {
context.__owl__.refs = context.__owl__.refs || {};
context = Object.create(context);
var h = this.h;
let c1 = [], p1 = {key:1};
@@ -1906,7 +1984,7 @@ exports[`t-ref refs in a loop 1`] = `
const ref6 = (context['item']);
p5.hook = {
create: (_, n) => {
context.refs[ref6] = n.elm;
context.__owl__.refs[ref6] = n.elm;
},
};
var _7 = context['item'];
+42 -5
View File
@@ -49,6 +49,11 @@ describe("static templates", () => {
qweb.addTemplate("test", "<div><span>word</span></div>");
expect(renderToString(qweb, "test")).toBe("<div><span>word</span></div>");
});
test("ignore comments", () => {
qweb.addTemplate("test", "<div>hello <!-- comment-->owl</div>");
expect(renderToString(qweb, "test")).toBe("<div>hello owl</div>");
});
});
describe("error handling", () => {
@@ -631,7 +636,9 @@ describe("t-call (template calling", () => {
`);
const root = { val: "a", children: [{ val: "b" }, { val: "c" }] };
const expected = "<div><div><p>a</p><div><p>b</p></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
expect(renderToString(qweb, "Parent", { root }, { fiber: { vars: {}, scope: {} } })).toBe(
expected
);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
expect(recursiveFn.toString()).toMatchSnapshot();
});
@@ -658,10 +665,17 @@ describe("t-call (template calling", () => {
const root = { val: "a", children: [{ val: "b", children: [{ val: "d" }] }, { val: "c" }] };
const expected =
"<div><div><p>a</p><div><p>b</p><div><p>d</p></div></div><div><p>c</p></div></div></div>";
expect(renderToString(qweb, "Parent", { root })).toBe(expected);
expect(renderToString(qweb, "Parent", { root }, { fiber: {} })).toBe(expected);
const recursiveFn = Object.values(qweb.recursiveFns)[0];
expect(recursiveFn.toString()).toMatchSnapshot();
});
test("t-call, global templates", () => {
QWeb.registerTemplate("abcd", '<div><t t-call="john"/></div>');
qweb.addTemplate("john", `<span>desk</span>`);
const expected = "<div><span>desk</span></div>";
expect(trim(renderToString(qweb, "abcd"))).toBe(expected);
});
});
describe("foreach", () => {
@@ -1078,14 +1092,14 @@ describe("t-ref", () => {
test("can get a ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
let refs: any = {};
renderToDOM(qweb, "test", { refs });
renderToDOM(qweb, "test", { __owl__: { refs } });
expect(refs.myspan.tagName).toBe("SPAN");
});
test("can get a dynamic ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
let refs: any = {};
renderToDOM(qweb, "test", { refs, id: 3 });
renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
expect(refs.myspan3.tagName).toBe("SPAN");
});
@@ -1100,7 +1114,7 @@ describe("t-ref", () => {
</div>`
);
let refs: any = {};
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] });
renderToDOM(qweb, "test", { items: [1, 2, 3], __owl__: { refs } });
expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
});
});
@@ -1298,3 +1312,26 @@ describe("global template registration", () => {
expect((vnode as any).children[0].text).toBe("global");
});
});
describe("properly support svg", () => {
test("add proper namespace to svg", () => {
qweb.addTemplate(
"test",
`<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`
);
expect(renderToString(qweb, "test")).toBe(
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
);
});
test("add proper namespace to g tags", () => {
// this is necessary if one wants to use components in a svg
qweb.addTemplate(
"test",
`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`
);
expect(renderToString(qweb, "test")).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
);
});
});
+2 -2
View File
@@ -30,7 +30,7 @@ describe("Link component", () => {
</div>
</templates>
`);
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { Link: Link };
}
@@ -61,7 +61,7 @@ describe("Link component", () => {
</div>
</templates>
`);
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { Link: Link };
}
+7 -7
View File
@@ -32,9 +32,9 @@ describe("RouteComponent", () => {
<span t-name="Users">Users</span>
</templates>
`);
class About extends Component<any, any, any> {}
class Users extends Component<any, any, any> {}
class App extends Component<any, any, any> {
class About extends Component<any, any> {}
class Users extends Component<any, any> {}
class App extends Component<any, any> {
static components = { RouteComponent };
}
@@ -64,8 +64,8 @@ describe("RouteComponent", () => {
<span t-name="Book">Book <t t-esc="props.title"/></span>
</templates>
`);
class Book extends Component<any, any, any> {}
class App extends Component<any, any, any> {
class Book extends Component<any, any> {}
class App extends Component<any, any> {
static components = { RouteComponent };
}
@@ -86,12 +86,12 @@ describe("RouteComponent", () => {
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
</templates>
`);
class Book extends Component<any, any, any> {
class Book extends Component<any, any> {
get incVal() {
return this.props.val + 1;
}
}
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { RouteComponent };
}
@@ -8,52 +8,37 @@ exports[`RouteComponent can render simple cases 1`] = `
let parent = context;
let owner = context;
let result;
context = Object.create(context);
var h = this.h;
var _1 = context['routes'];
if (!_1) { throw new Error('QWeb error: Invalid loop expression')}
var _2 = _3 = _1;
if (!(_1 instanceof Array)) {
_2 = Object.keys(_1);
_3 = Object.values(_1);
}
var _length2 = _2.length;
for (let i = 0; i < _length2; i++) {
context.route_first = i === 0;
context.route_last = i === _length2 - 1;
context.route_index = i;
context.route = _2[i];
context.route_value = _3[i];
if (context['env'].router.currentRouteName===context['route'].name) {
//COMPONENT
let def5;
let templateId7 = String(-6 - i);
let w6 = templateId7 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId7]] : false;
let vn8 = {};
result = vn8;
let props6 = Object.assign({}, context['env'].router.currentParams);
if (w6 && w6.__owl__.renderPromise && !w6.__owl__.vnode) {
if (utils.shallowEqual(props6, w6.__owl__.renderProps)) {
def5 = w6.__owl__.renderPromise;
} else {
w6.destroy();
w6 = false;
}
}
if (!w6) {
let componentKey6 = (context['route'].component);
let W6 = context.constructor.components[componentKey6] || QWeb.components[componentKey6];
if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')}
w6 = new W6(parent, props6);
parent.__owl__.cmap[templateId7] = w6.__owl__.id;
def5 = w6.__prepare();
def5 = def5.then(vnode=>{if (w6.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId7, hook: {insert(vn) {let nvn=w6.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w6.destroy();}}});utils.defineProxy(vn8, pvnode);w6.__owl__.pvnode = pvnode;});
if (context['routeComponent']) {
//COMPONENT
let key4 = 'key' + context['env'].router.currentRouteName;
let def2;
let templateId5 = key4;
let w3 = templateId5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId5]] : false;
let vn6 = {};
result = vn6;
let props3 = Object.assign({}, context['env'].router.currentParams);
if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) {
if (utils.shallowEqual(props3, w3.__owl__.currentFiber.props)) {
def2 = w3.__owl__.currentFiber.promise;
} else {
def5 = def5 || w6.__updateProps(props6, extra.forceUpdate, extra.patchQueue);
def5 = def5.then(()=>{if (w6.__owl__.isDestroyed) {return};let pvnode=w6.__owl__.pvnode;utils.defineProxy(vn8, pvnode);});
w3.destroy();
w3 = false;
}
extra.promises.push(def5);
}
if (!w3) {
let componentKey3 = \`routeComponent\`;
let W3 = context.constructor.components[componentKey3] || QWeb.components[componentKey3]|| context['routeComponent'];
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap[templateId5] = w3.__owl__.id;
def2 = w3.__prepare(extra.fiber, undefined, undefined);
def2 = def2.then(vnode=>{if (w3.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId5, hook: {insert(vn) {let nvn=w3.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w3.destroy();}}});utils.defineProxy(vn6, pvnode);w3.__owl__.pvnode = pvnode;});
} else {
def2 = def2 || w3.__updateProps(props3, extra.fiber, undefined, undefined);
def2 = def2.then(()=>{if (w3.__owl__.isDestroyed) {return};let pvnode=w3.__owl__.pvnode;utils.defineProxy(vn6, pvnode);});
}
extra.promises.push(def2);
}
return result;
}"
+56 -53
View File
@@ -1,6 +1,7 @@
import { Component, Env } from "../../src/component/component";
import { ConnectedComponent } from "../../src/store/connected_component";
import { Store } from "../../src/store/store";
import { useState } from "../../src/hooks";
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers";
describe("connecting a component to store", () => {
@@ -27,8 +28,8 @@ describe("connecting a component to store", () => {
<span t-name="Todo"><t t-esc="props.msg"/></span>
</templates>
`);
class Todo extends Component<any, any, any> {}
class App extends ConnectedComponent<any, any, any> {
class Todo extends Component<any, any> {}
class App extends ConnectedComponent<any, any> {
static components = { Todo };
static mapStoreToProps(s) {
return { todos: s.todos };
@@ -70,7 +71,7 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, actions });
class App extends ConnectedComponent<any, any, any> {
class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) {
return { todos: s.todos };
}
@@ -111,7 +112,7 @@ describe("connecting a component to store", () => {
<span t-name="Todo"><t t-esc="props.msg"/></span>
</templates>
`);
class Todo extends Component<any, any, any> {}
class Todo extends Component<any, any> {}
const store = new Store({
state: { todos: [] },
@@ -121,7 +122,7 @@ describe("connecting a component to store", () => {
}
}
});
class App extends ConnectedComponent<any, any, any> {
class App extends ConnectedComponent<any, any> {
static components = { Todo };
static mapStoreToProps(s) {
return { todos: s.todos };
@@ -160,7 +161,7 @@ describe("connecting a component to store", () => {
});
(<any>env).store = store;
class App extends ConnectedComponent<any, any, any> {
class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) {
return { value: s.value };
}
@@ -187,7 +188,7 @@ describe("connecting a component to store", () => {
<div t-name="Child"/>
</templates>
`);
class Child extends ConnectedComponent<any, any, any> {
class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s) {
return s;
}
@@ -199,12 +200,12 @@ describe("connecting a component to store", () => {
}
}
class Parent extends Component<any, any, any> {
class Parent extends Component<any, any> {
static components = { Child };
state: any;
constructor(env: Env) {
super(env);
this.state = { child: true };
this.state = useState({ child: true });
}
}
@@ -240,14 +241,14 @@ describe("connecting a component to store", () => {
</div>
</templates>
`);
class TodoItem extends ConnectedComponent<any, any, any> {
class TodoItem extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) {
const todo = state.todos.find(t => t.id === props.id);
return todo;
}
}
class TodoList extends ConnectedComponent<any, any, any> {
class TodoList extends ConnectedComponent<any, any> {
static components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
@@ -294,7 +295,7 @@ describe("connecting a component to store", () => {
</templates>
`);
class TodoItem extends ConnectedComponent<any, any, any> {
class TodoItem extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props, getters) {
const todo = state.todos.find(t => t.id === props.id);
return {
@@ -304,7 +305,7 @@ describe("connecting a component to store", () => {
}
}
class TodoList extends ConnectedComponent<any, any, any> {
class TodoList extends ConnectedComponent<any, any> {
static components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
@@ -330,15 +331,15 @@ describe("connecting a component to store", () => {
</templates>
`);
class Beer extends ConnectedComponent<any, any, any> {
class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) {
return state.beers[props.id];
}
}
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { Beer };
state = { beerId: 1 };
state = useState({ beerId: 1 });
}
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
@@ -363,7 +364,7 @@ describe("connecting a component to store", () => {
</templates>
`);
class App extends ConnectedComponent<any, any, any> {
class App extends ConnectedComponent<any, any> {
static mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 };
}
@@ -403,7 +404,7 @@ describe("connecting a component to store", () => {
</templates>
`);
class Beer extends ConnectedComponent<any, any, any> {
class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) {
return {
selected: state.beers[props.id],
@@ -413,9 +414,9 @@ describe("connecting a component to store", () => {
}
}
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { Beer };
state = { beerId: 0 };
state = useState({ beerId: 0 });
}
const actions = {
@@ -470,7 +471,7 @@ describe("connecting a component to store", () => {
</templates>
`);
class Beer extends ConnectedComponent<any, any, any> {
class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(storeState, props) {
return {
selected: storeState.beers[props.id],
@@ -480,9 +481,9 @@ describe("connecting a component to store", () => {
}
}
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { Beer };
state = { beerId: 0 };
state = useState({ beerId: 0 });
}
const actions = {
@@ -559,13 +560,13 @@ describe("connecting a component to store", () => {
</templates>
`);
class Child extends ConnectedComponent<any, any, any> {
class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s, props) {
steps.push("child");
return { msg: s.msg[props.key] };
}
}
class Parent extends ConnectedComponent<any, any, any> {
class Parent extends ConnectedComponent<any, any> {
static components = { Child };
static mapStoreToProps(s) {
steps.push("parent");
@@ -608,13 +609,13 @@ describe("connecting a component to store", () => {
</templates>
`);
class Child extends ConnectedComponent<any, any, any> {
class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s, props) {
steps.push("child");
return { msg: s.messages[props.someId] };
}
}
class Parent extends ConnectedComponent<any, any, any> {
class Parent extends ConnectedComponent<any, any> {
static components = { Child };
static mapStoreToProps(s) {
steps.push("parent");
@@ -686,7 +687,7 @@ describe("connecting a component to store", () => {
let renderCount = 0;
let fCount = 0;
class TodoItem extends ConnectedComponent<any, any, any> {
class TodoItem extends ConnectedComponent<any, any> {
state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
fCount++;
@@ -698,12 +699,12 @@ describe("connecting a component to store", () => {
editTodo() {
this.env.store.dispatch("editTodo");
}
__render(...args) {
__render(f) {
renderCount++;
return super.__render(...args);
return super.__render(f);
}
}
class TodoApp extends ConnectedComponent<any, any, any> {
class TodoApp extends ConnectedComponent<any, any> {
static components = { TodoItem };
static mapStoreToProps(state) {
return {
@@ -764,7 +765,7 @@ describe("connecting a component to store", () => {
let renderCount = 0;
let fCount = 0;
class TodoItem extends ConnectedComponent<any, any, any> {
class TodoItem extends ConnectedComponent<any, any> {
state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
@@ -776,13 +777,13 @@ describe("connecting a component to store", () => {
removeTodo() {
this.env.store.dispatch("removeTodo");
}
__render(...args) {
__render(f) {
renderCount++;
return super.__render(...args);
return super.__render(f);
}
}
class TodoApp extends ConnectedComponent<any, any, any> {
class TodoApp extends ConnectedComponent<any, any> {
static components = { TodoItem };
static mapStoreToProps(state) {
return {
@@ -817,7 +818,7 @@ describe("connecting a component to store", () => {
</templates>
`);
class App extends ConnectedComponent<any, any, any> {
class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) {
return { msg: s.msg };
}
@@ -871,11 +872,11 @@ describe("connected components and default values", () => {
</templates>
`);
class Greeter extends ConnectedComponent<any, any, any> {
class Greeter extends ConnectedComponent<any, any> {
static defaultProps = { recipient: "John" };
}
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { Greeter };
}
@@ -895,11 +896,11 @@ describe("connected components and default values", () => {
</templates>
`);
class Greeter extends ConnectedComponent<any, any, any> {
class Greeter extends ConnectedComponent<any, any> {
static defaultProps = { recipient: "John" };
}
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { Greeter };
}
@@ -910,11 +911,12 @@ describe("connected components and default values", () => {
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
await app.__updateProps({ initialRecipient: "James" }, true);
const fiber = app.__createFiber(true, undefined, undefined, undefined);
await app.__updateProps({ initialRecipient: "James" }, fiber);
await app.render();
expect(fixture.innerHTML).toBe("<div><div>Hello, James</div></div>");
await app.__updateProps({ initialRecipient: undefined }, true);
await app.__updateProps({ initialRecipient: undefined }, fiber);
await app.render();
expect(fixture.innerHTML).toBe("<div><div>Hello, John</div></div>");
});
@@ -935,7 +937,7 @@ describe("connected components and default values", () => {
</templates>
`);
class Message extends ConnectedComponent<any, any, any> {
class Message extends ConnectedComponent<any, any> {
static defaultProps = { showId: true };
static mapStoreToProps = function(state, ownProps) {
return {
@@ -944,7 +946,7 @@ describe("connected components and default values", () => {
};
}
class Thread extends ConnectedComponent<any, any, any> {
class Thread extends ConnectedComponent<any, any> {
static components = { Message };
static defaultProps = { showMessages: true };
static mapStoreToProps = function(state, ownProps) {
@@ -955,7 +957,7 @@ describe("connected components and default values", () => {
};
}
class App extends Component<any, any, any> {
class App extends Component<any, any> {
static components = { Thread };
static defaultProps = { threadId: 1 };
}
@@ -997,7 +999,8 @@ describe("connected components and default values", () => {
"<div><div><div>100Message100</div><div>101Message101</div></div></div>"
);
await app.__updateProps({ threadId: 2 }, true);
const fiber = app.__createFiber(true, undefined, undefined, undefined);
await app.__updateProps({ threadId: 2 }, fiber);
await app.render();
expect(fixture.innerHTML).toBe("<div><div><div>200Message200</div></div></div>");
@@ -1016,15 +1019,15 @@ describe("connected components and default values", () => {
<div t-name="Child"><t t-esc="storeProps.val"/></div>
</templates>
`);
class Child extends ConnectedComponent<any, any, any> {
class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s) {
return s;
}
}
class Parent extends Component<any, any, any> {
class Parent extends Component<any, any> {
static components = { Child };
state = { child: true };
state = useState({ child: true });
}
class TestStore extends Store {
@@ -1060,7 +1063,7 @@ describe("connected components and default values", () => {
</templates>
`);
class App extends ConnectedComponent<any, any, any> {
class App extends ConnectedComponent<any, any> {
static mapStoreToProps = function(state) {
return {
counter: state.counter
@@ -1141,14 +1144,14 @@ describe("various scenarios", () => {
</div>
</templates>
`);
class Attachment extends ConnectedComponent<any, any, any> {
class Attachment extends ConnectedComponent<any, any> {
static mapStoreToProps(state, ownProps) {
return {
name: state.attachments[ownProps.id].name
};
}
}
class Message extends ConnectedComponent<any, any, any> {
class Message extends ConnectedComponent<any, any> {
static mapStoreToProps(state) {
return {
attachmentIds: state.messages[10].attachmentIds
+7 -11
View File
@@ -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() {
+153
View File
@@ -0,0 +1,153 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.21.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+7 -5
View File
@@ -1,10 +1,11 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
state = useState({ counter: 0 });
increment() {
this.state.counter++;
@@ -32,7 +33,8 @@ class Message extends owl.Component {
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -129,12 +131,12 @@ class App extends owl.Component {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
this.logRef.el.innerHTML = "";
}
}
+2 -2
View File
@@ -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>
+1
View File
@@ -39,6 +39,7 @@
<li><a href="benchmarks/owl-0.16.0">OWL 0.16.0</a></li>
<li><a href="benchmarks/owl-0.17.0">OWL 0.17.0</a></li>
<li><a href="benchmarks/owl-0.18.0">OWL 0.18.0</a></li>
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+11 -10
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js";
const {useState, useRef} = owl.hooks;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
@@ -156,18 +156,18 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
this.state = {
this.state = useState({
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
};
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
this._setupSessions(props);
this.editor = null;
this.editorNode = useRef("editor");
}
mounted() {
this.editor = this.editor || ace.edit(this.refs.editor);
this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
@@ -250,7 +250,7 @@ class App extends owl.Component {
this.version = owl.__info__.version;
this.SAMPLES = SAMPLES;
this.state = {
this.state = useState({
js: SAMPLES[0].code,
css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML,
@@ -259,18 +259,19 @@ class App extends owl.Component {
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
};
});
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
}
displayError(error) {
this.state.error = error;
if (error) {
setTimeout(() => {
this.refs.content.innerHTML = "";
this.content.el.innerHTML = "";
});
return;
}
@@ -296,8 +297,8 @@ class App extends owl.Component {
} else {
this.state.error = false;
}
this.refs.content.innerHTML = "";
this.refs.content.appendChild(subiframe);
this.content.el.innerHTML = "";
this.content.el.appendChild(subiframe);
}
setSample(ev) {
+3 -1
View File
@@ -173,8 +173,10 @@ body {
}
.right-pane .note {
font-size: 20px;
font-size: 18px;
text-align: left;
margin-top: 20px;
padding: 5%;
}
.right-pane .error {
+272 -53
View File
@@ -1,17 +1,18 @@
const COMPONENTS = `// In this example, we show how components can be defined and created.
const { Component, useState } = owl;
class Greeter extends owl.Component {
state = { word: 'Hello' };
class Greeter extends Component {
state = useState({ word: 'Hello' });
toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
}
}
// Main root component
class App extends owl.Component {
class App extends Component {
static components = { Greeter };
state = { name: 'World'};
state = useState({ name: 'World'});
}
// Application setup
@@ -45,17 +46,18 @@ const COMPONENTS_CSS = `.greeter {
const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects.
const { Component, useState } = owl;
class Counter extends owl.Component {
state = { value: 0 };
class Counter extends Component {
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
class App extends owl.Component {
state = { flag: false, componentFlag: false, numbers: [] };
class App extends Component {
state = useState({ flag: false, componentFlag: false, numbers: [] });
static components = { Counter };
toggle(key) {
@@ -183,11 +185,12 @@ const LIFECYCLE_DEMO = `// This example shows all the possible lifecycle hooks
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
const { Component, useState } = owl;
class DemoComponent extends owl.Component {
class DemoComponent extends Component {
constructor() {
super(...arguments);
this.state = { n: 0 };
this.state = useState({ n: 0 });
console.log("constructor");
}
async willStart() {
@@ -213,9 +216,9 @@ class DemoComponent extends owl.Component {
}
}
class App extends owl.Component {
class App extends Component {
static components = { DemoComponent };
state = { n: 0, flag: true };
state = useState({ n: 0, flag: true });
increment() {
this.state.n++;
@@ -259,12 +262,70 @@ const LIFECYCLE_CSS = `button {
width: 250px;
}`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const {useState, onMounted, onWillUnmount} = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onWillUnmount(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class App extends owl.Component {
// simple state hook (reactive object)
counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
increment() {
this.counter.value++;
}
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
`;
const HOOKS_DEMO_XML = `<templates>
<div t-name="App">
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
`;
const HOOKS_CSS = `button {
width: 120px;
height: 35px;
font-size: 16px;
}`;
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
// www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence.
//
// In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store.
const { Component, useState } = owl;
const { useRef } = owl.hooks;
const ENTER_KEY = 13;
const ESC_KEY = 27;
@@ -335,8 +396,9 @@ function makeStore() {
//------------------------------------------------------------------------------
// TodoItem
//------------------------------------------------------------------------------
class TodoItem extends owl.Component {
state = { isEditing: false };
class TodoItem extends Component {
state = useState({ isEditing: false });
inputRef = useRef("input");
removeTodo() {
this.env.store.dispatch("removeTodo", this.props.id);
@@ -351,9 +413,9 @@ class TodoItem extends owl.Component {
}
focusInput() {
this.refs.input.value = "";
this.refs.input.focus();
this.refs.input.value = this.props.title;
this.inputRef.el.value = "";
this.inputRef.el.focus();
this.inputRef.el.value = this.props.title;
}
handleKeyup(ev) {
@@ -389,7 +451,7 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------
class TodoApp extends owl.store.ConnectedComponent {
static components = { TodoItem };
state = { filter: "all" };
state = useState({ filter: "all" });
static mapStoreToProps(state) {
return {
@@ -1052,17 +1114,18 @@ const SLOTS = `// We show here how slots can be used to create generic component
//
// Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component
const { Component, useState } = owl;
class Card extends owl.Component {
state = { showContent: true };
class Card extends Component {
state = useState({ showContent: true });
toggleDisplay() {
this.state.showContent = !this.state.showContent;
}
}
class Counter extends owl.Component {
state = {val: 1};
class Counter extends Component {
state = useState({val: 1});
inc() {
this.state.val++;
@@ -1070,9 +1133,9 @@ class Counter extends owl.Component {
}
// Main root component
class App extends owl.Component {
class App extends Component {
static components = {Card, Counter};
state = {a: 1, b: 3};
state = useState({a: 1, b: 3});
inc(key, delta) {
this.state[key] += delta;
@@ -1168,8 +1231,9 @@ const ASYNC_COMPONENTS = `// This example will not work if your browser does not
// However, we don't want renderings of the other sub component to be delayed
// because of the slow component. We use the 't-asyncroot' directive for this
// purpose. Try removing it to see the difference.
const { Component, useState } = owl;
class SlowComponent extends owl.Component {
class SlowComponent extends Component {
willUpdateProps() {
// simulate a component that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself
@@ -1177,11 +1241,11 @@ class SlowComponent extends owl.Component {
}
}
class NotificationList extends owl.Component {}
class NotificationList extends Component {}
class App extends owl.Component {
class App extends Component {
static components = {SlowComponent, NotificationList};
state = { value: 0, notifs: [] };
state = useState({ value: 0, notifs: [] });
increment() {
this.state.value++;
@@ -1250,15 +1314,16 @@ const FORM = `// This example illustrate how the t-model directive can be used t
// data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally
// synchronized.
const { Component, useState } = owl;
class Form extends owl.Component {
state = {
class Form extends Component {
state = useState({
text: "",
othertext: "",
number: 11,
color: "",
bool: false
};
});
}
// Application setup
@@ -1271,20 +1336,20 @@ const FORM_XML = `<templates>
<div t-name="Form">
<h1>Form</h1>
<div>
Text (immediate): <input t-model="text"/>
Text (immediate): <input t-model="state.text"/>
</div>
<div>
Other text (lazy): <input t-model.lazy="othertext"/>
Other text (lazy): <input t-model.lazy="state.othertext"/>
</div>
<div>
Number: <input t-model.number="number"/>
Number: <input t-model.number="state.number"/>
</div>
<div>
Boolean: <input type="checkbox" t-model="bool"/>
Boolean: <input type="checkbox" t-model="state.bool"/>
</div>
<div>
Color, with a select:
<select t-model="color">
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
@@ -1292,8 +1357,8 @@ const FORM_XML = `<templates>
</div>
<div>
Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="color"/><label for="blue">Blue</label></span>
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
</div>
<hr/>
<h1>State</h1>
@@ -1316,26 +1381,31 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - minimal width/height
// - better heuristic for initial window position
// - ...
const { Component, useState } = owl;
const { useRef } = owl.hooks;
class HelloWorld extends owl.Component {}
class HelloWorld extends Component {}
class Counter extends owl.Component {
state = { value: 0 };
class Counter extends Component {
state = useState({ value: 0 });
inc() {
this.state.value++;
}
}
class Window extends owl.Component {
class Window extends Component {
get style() {
let { width, height, top, left, zindex } = this.props.info;
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
}
close() {
this.trigger("close-window", { id: this.props.info.id });
}
startDragAndDrop(ev) {
this.updateZIndex();
this.el.classList.add('dragging');
@@ -1361,31 +1431,37 @@ class Window extends owl.Component {
self.trigger("set-window-position", options);
}
}
updateZIndex() {
this.trigger("update-z-index", { id: this.props.info.id });
}
}
class WindowManager extends owl.Component {
class WindowManager extends Component {
static components = { Window };
windows = [];
nextId = 1;
currentZindex = 1;
nextLeft = 0;
nextTop = 0;
addWindow(name) {
const info = this.env.windows.find(w => w.name === name);
const id = \`w\${this.nextId++}\`;
this.nextLeft = this.nextLeft + 30;
this.nextTop = this.nextTop + 30;
this.windows.push({
id: id,
id: this.nextId++,
title: info.title,
width: info.defaultWidth,
height: info.defaultHeight,
top: 0,
left: 0,
zindex: this.currentZindex++
top: this.nextTop,
left: this.nextLeft,
zindex: this.currentZindex++,
component: info.component
});
this.constructor.components[id] = info.component;
this.render();
}
closeWindow(ev) {
const id = ev.detail.id;
delete this.constructor.components[id];
@@ -1393,12 +1469,14 @@ class WindowManager extends owl.Component {
this.windows.splice(index, 1);
this.render();
}
setWindowPosition(ev) {
const id = ev.detail.id;
const w = this.windows.find(w => w.id === id);
w.top = ev.detail.top;
w.left = ev.detail.left;
}
updateZIndex(ev) {
const id = ev.detail.id;
const w = this.windows.find(w => w.id === id);
@@ -1407,11 +1485,12 @@ class WindowManager extends owl.Component {
}
}
class App extends owl.Component {
class App extends Component {
static components = { WindowManager };
wmRef = useRef("wm");
addWindow(name) {
this.refs.wm.addWindow(name);
this.wmRef.comp.addWindow(name);
}
}
@@ -1452,7 +1531,7 @@ const WMS_XML = `<templates>
t-on-update-z-index="updateZIndex"
t-on-set-window-position="setWindowPosition">
<Window t-foreach="windows" t-as="w" t-key="w.id" info="w">
<t t-component="{{w.id}}"/>
<t t-component="w.component"/>
</Window>
</div>
@@ -1546,6 +1625,135 @@ const WMS_CSS = `body {
font-size: 20px;
}`;
const MONKEYPATCH = `
// In this example, we show a possible way components can be monkey patched.
// This involves the functions patch/unpatch, which modifies the prototype of a
// class (not only an owl component). Sadly, the patch code cannot make use of
// the syntactic sugar that JS engines provides to classes, so it needs to use
// the "this._super()" syntax instead of the standard "super.someMethod()".
//--------------------------------------------------------------------------
// Patching Code
//--------------------------------------------------------------------------
const patchMap = new WeakMap();
function patch(C, patchName, patch) {
let metadata = patchMap.get(C.prototype);
if (!metadata) {
metadata = {
origMethods: {},
patches: {},
current: []
};
patchMap.set(C.prototype, metadata);
}
const proto = C.prototype;
if (metadata.patches[patchName]) {
throw new Error(\`Patch [\${patchName}] already exists\`);
}
metadata.patches[patchName] = patch;
applyPatch(proto, patch);
metadata.current.push(patchName);
function applyPatch(proto, patch) {
Object.keys(patch).forEach(function(methodName) {
const method = patch[methodName];
if (typeof method === "function") {
const original = proto[methodName];
if (!(methodName in metadata.origMethods)) {
metadata.origMethods[methodName] = original;
}
proto[methodName] = function(...args) {
this._super = original;
return method.call(this, ...args);
};
}
});
}
}
// we define here an unpatch function. This is mostly useful if we want to
// remove a patch. For example, for testing purposes
function unpatch(C, patchName) {
const proto = C.prototype;
let metadata = patchMap.get(proto);
if (!metdata) {
return;
}
patchMap.delete(proto);
// reset to original
for (let k in metadata.origMethods) {
proto[k] = metadata.origMethods[k];
}
// apply other patches
for (let name of metadata.current) {
if (name !== patchName) {
patch(C, name, metadata.patches[name]);
}
}
}
//--------------------------------------------------------------------------
// Components
//--------------------------------------------------------------------------
const { Component, useState } = owl;
class Counter extends Component {
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
// Main root component
class App extends Component {
static components = { Counter };
}
//--------------------------------------------------------------------------
// Patching code
//--------------------------------------------------------------------------
// In an Odoo application, this code would be located in another odoo module,
// to customize some behaviour in an existing class/component
patch(Counter, "double_value", {
increment() {
this._super();
this.state.value = 2 * this.state.value;
}
});
//--------------------------------------------------------------------------
// Application setup
//--------------------------------------------------------------------------
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
`;
const MONKEYPATCH_XML = `
<templates>
<button t-name="Counter" t-on-click="increment">
Click [<t t-esc="state.value"/>]
</button>
<div t-name="App">
<Counter />
</div>
</templates>
`;
const MONKEYPATCH_CSS = `
button {
font-size: 20px;
width: 100px;
height: 50px;
}`;
export const SAMPLES = [
{
description: "Components",
@@ -1570,6 +1778,12 @@ export const SAMPLES = [
xml: LIFECYCLE_DEMO_XML,
css: LIFECYCLE_CSS
},
{
description: "Hooks",
code: HOOKS_DEMO,
xml: HOOKS_DEMO_XML,
css: HOOKS_CSS
},
{
description: "Todo List App (with store)",
code: TODO_APP_STORE,
@@ -1599,5 +1813,10 @@ export const SAMPLES = [
code: ASYNC_COMPONENTS,
xml: ASYNC_COMPONENTS_XML,
css: ASYNC_COMPONENTS_CSS
}, {
description: "Monkey patching components",
code: MONKEYPATCH,
xml: MONKEYPATCH_XML,
css: MONKEYPATCH_CSS
}
];
+5 -1
View File
@@ -45,7 +45,11 @@
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
<div class="note">
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
</p>
</div>
</div>
<div t-if="state.error" class="error">
<h3>Error</h3>