mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
@@ -20,6 +20,7 @@ Here is a short example to illustrate interactive components:
|
||||
```javascript
|
||||
import { Component, QWeb } from 'owl'
|
||||
import { xml } from 'owl/tags'
|
||||
import { useState } from 'owl/hooks'
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
@@ -27,7 +28,7 @@ class Counter extends Component {
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = { value: 0 };
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
@@ -48,6 +49,8 @@ const app = new App({ qweb: new QWeb() });
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
Note that the counter component is made reactive with the `useState` hook.
|
||||
|
||||
More interesting examples can be found on the
|
||||
[playground](https://odoo.github.io/owl/playground) application.
|
||||
|
||||
|
||||
+51
-39
@@ -45,16 +45,16 @@ 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 +74,27 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Reference
|
||||
|
||||
@@ -96,10 +114,6 @@ 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.
|
||||
@@ -236,7 +250,7 @@ component.
|
||||
```javascript
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = {someValue: true};
|
||||
this.state = useState({someValue: true});
|
||||
this.template = 'mytemplate';
|
||||
}
|
||||
```
|
||||
@@ -246,7 +260,7 @@ implemented in most cases:
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
state = { value: 0 };
|
||||
state = useState({ value: 0 });
|
||||
|
||||
...
|
||||
}
|
||||
@@ -271,9 +285,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 +298,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,7 +325,7 @@ It is not called on the initial render. This is useful to read some
|
||||
information from the DOM. For example, the current position of the
|
||||
scrollbar.
|
||||
|
||||
Note that modifying the state object is not allowed here. This method is called just
|
||||
Note that modifying the state is not allowed here. This method is called just
|
||||
before an actual DOM patch, and is only intended to be used to save some local
|
||||
DOM state. Also, it will not be called if the component is not in the DOM (this can
|
||||
happen with components with `t-keepalive`).
|
||||
@@ -397,9 +407,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 +418,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 +452,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 +464,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 });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -642,7 +653,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;
|
||||
@@ -662,8 +673,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 {
|
||||
@@ -673,7 +685,7 @@ class Form extends owl.Component {
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-model="text" />
|
||||
<input t-model="state.text" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
@@ -683,11 +695,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>
|
||||
@@ -696,11 +708,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>
|
||||
@@ -718,7 +730,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
|
||||
@@ -1117,8 +1129,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;
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
# 🦉 Hooks 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [One Rule](#one-rule)
|
||||
- [`useState`](#usestate)
|
||||
- [`onMounted`](#onmounted)
|
||||
- [`onWillUnmount`](#onWillUnmount)
|
||||
|
||||
## Overview
|
||||
|
||||
Hooks were popularised by React as a way to solve the following issues:
|
||||
- help reusing stateful logic between components
|
||||
- help organizing code by feature in complex components
|
||||
- use state in functional components, without writing a class.
|
||||
|
||||
Owl hooks serve the same purpose, except that they work for class components
|
||||
(note: React hooks do not work on class components, and maybe because of that,
|
||||
there seems to be the misconception that hooks are in opposition to class. This
|
||||
is clearly not true, as shown by Owl hooks).
|
||||
|
||||
|
||||
Hooks works beautifully with Owl components: they solve the problems mentioned
|
||||
above, and in particular, they are the perfect way to make your component
|
||||
reactive.
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
```js
|
||||
const {useState, onMounted, onWillUnmount} = owl.hooks;
|
||||
|
||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||
// position
|
||||
function useMouse() {
|
||||
const position = useState({x:0, y: 0});
|
||||
|
||||
function update(e) {
|
||||
position.x = e.clientX;
|
||||
position.y = e.clientY;
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', update);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
window.removeEventListener('mousemove', update);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
static template = xml`
|
||||
<div t-name="App">
|
||||
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||
</div>`;
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
mouse = useMouse();
|
||||
}
|
||||
```
|
||||
|
||||
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||
a convention.
|
||||
|
||||
## Reference
|
||||
|
||||
### One rule
|
||||
|
||||
There is only one rule: every hook for a component have to be called in the
|
||||
constructor (or in class fields):
|
||||
|
||||
```js
|
||||
// ok
|
||||
class SomeComponent extends Component {
|
||||
state = useState();
|
||||
}
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = useState();
|
||||
}
|
||||
}
|
||||
|
||||
// not ok: this is executed after the constructor is called
|
||||
class SomeComponent extends Component {
|
||||
async willStart() {
|
||||
this.state = useState();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `useState`
|
||||
|
||||
The `useState` hook is certainly the most important hooks for Owl components:
|
||||
this is what enables component to be reactive, to react to state change.
|
||||
|
||||
The `useState` hook has to be given an object or an array, and will return
|
||||
an observed version of it (using a `Proxy`).
|
||||
|
||||
```javascript
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends owl.Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `onMounted`
|
||||
|
||||
`onMounted` is not an hook, but is a building block designed to help make useful
|
||||
hooks. `onMounted` register a callback, which will be called when the component
|
||||
is mounted (see example on top of this page).
|
||||
|
||||
### `onWillUnmount`
|
||||
|
||||
`onWillUnmount` is not an hook, but is a building block designed to help make useful
|
||||
hooks. `onWillUnmount` register a callback, which will be called when the component
|
||||
is unmounted (see example on top of this page).
|
||||
+3
-33
@@ -11,40 +11,10 @@ For example, this code will display `update` in the console:
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
observer.notifyCB = () => console.log("update");
|
||||
observer.observe(obj);
|
||||
const obj = observer.observe( { a: { b: 1 } });
|
||||
|
||||
const obj = { a: { b: 1 } };
|
||||
obj.a.b = 2;
|
||||
```
|
||||
|
||||
## Technical Limitations
|
||||
|
||||
Since the observer uses getters and setters, it is actually unable to react to
|
||||
changes in three situations:
|
||||
|
||||
- adding a key to an object
|
||||
- deleting a key from an object
|
||||
- modifying an array by setting a new value at a given index
|
||||
|
||||
In those situations, we need a way to tell the observer that something happened.
|
||||
This can be done by using the `set` and `delete` (only for objects) static
|
||||
methods of the `Observer`.
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const obj = { a: 1 };
|
||||
observer.observe(obj);
|
||||
obj.b = 2; // won't notify the change
|
||||
owl.Observer.set(obj, "b", 2); // will notify the change
|
||||
|
||||
delete obj.b; // won't notify the change
|
||||
owl.Observer.delete(obj, "b"); // will notify the change
|
||||
```
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const arr = ["a"];
|
||||
observer.observe(arr);
|
||||
arr[0] = "b"; // won't notify the change
|
||||
owl.Observer.set(arr, 0, "b"); // will notify the change
|
||||
```
|
||||
The observer is implemented with the native `Proxy` object. Note that this
|
||||
means that it will not work on older browsers.
|
||||
|
||||
+3
-1
@@ -72,11 +72,13 @@ Here are a few steps that we may take to get started:
|
||||
Let us now add the javascript to make it work, in `app.js`:
|
||||
|
||||
```javascript
|
||||
const useState = owl.hooks.useState;
|
||||
|
||||
class ClickCounter extends owl.Component {
|
||||
static template = "clickcounter";
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.state = { value: 0 };
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
|
||||
@@ -9,9 +9,14 @@ build applications. Here is a complete representation of its content:
|
||||
owl
|
||||
Component
|
||||
QWeb
|
||||
useState
|
||||
core
|
||||
EventBus
|
||||
Observer
|
||||
hooks
|
||||
onMounted
|
||||
onWillUnmount
|
||||
useState
|
||||
router
|
||||
Link
|
||||
RouteComponent
|
||||
@@ -29,11 +34,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)
|
||||
|
||||
+14
-26
@@ -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
|
||||
*/
|
||||
|
||||
@@ -46,7 +47,7 @@ export interface Fiber<Props> {
|
||||
scope: any;
|
||||
vars: any;
|
||||
patchQueue: Fiber<any>[];
|
||||
component: Component<any, any, any>;
|
||||
component: Component<any, any>;
|
||||
vnode: VNode | null;
|
||||
willPatchResult: any;
|
||||
props: Props;
|
||||
@@ -70,8 +71,8 @@ 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.
|
||||
@@ -91,10 +92,11 @@ 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;
|
||||
|
||||
/**
|
||||
* The `el` is the root element of the component. Note that it could be null:
|
||||
@@ -106,7 +108,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
static components = {};
|
||||
|
||||
env: T;
|
||||
state?: State;
|
||||
props: Props;
|
||||
|
||||
// type of props is not easily representable in typescript...
|
||||
@@ -114,7 +115,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
static defaultProps?: any;
|
||||
|
||||
refs: {
|
||||
[key: string]: Component<T, any, any> | HTMLElement | undefined;
|
||||
[key: string]: Component<T, any> | HTMLElement | undefined;
|
||||
} = {};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -140,8 +141,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);
|
||||
}
|
||||
@@ -151,7 +153,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;
|
||||
@@ -452,7 +454,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) {
|
||||
@@ -586,7 +588,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
}
|
||||
}
|
||||
__owl__.render = qweb.render.bind(qweb, p._template);
|
||||
this.__observeState();
|
||||
return this.__render(fiber);
|
||||
}
|
||||
|
||||
@@ -656,19 +657,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).
|
||||
*
|
||||
@@ -693,7 +681,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
||||
*/
|
||||
__applyPatchQueue(fiber: Fiber<Props>) {
|
||||
const patchQueue = fiber.patchQueue;
|
||||
let component: Component<any, any, any> = this;
|
||||
let component: Component<any, any> = this;
|
||||
try {
|
||||
const patchLen = patchQueue.length;
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
@@ -729,7 +717,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,7 +33,7 @@ export class Observer {
|
||||
}
|
||||
}
|
||||
|
||||
observe(value: any, parent?: any): any {
|
||||
observe<T>(value: T, parent?: any): T {
|
||||
if (value === null || typeof value !== "object" || value instanceof Date) {
|
||||
// fun fact: typeof null === 'object'
|
||||
return value;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* useState hook
|
||||
*
|
||||
* This is the main way a component can be made reactive. The useState hook
|
||||
* will return an observed object (or array). Changes to that value will then
|
||||
* trigger a rerendering of the current component.
|
||||
*/
|
||||
export function useState<T>(state: T): T {
|
||||
const component: Component<any,any> = Component._current;
|
||||
const __owl__ = component.__owl__;
|
||||
if (!__owl__.observer) {
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.notifyCB = component.render.bind(component);
|
||||
}
|
||||
return __owl__.observer.observe(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounted hook. The callback will be called when the current component is
|
||||
* mounted. Note that the component mounted method is called first.
|
||||
*/
|
||||
export function onMounted(cb) {
|
||||
const component = Component._current;
|
||||
const current = component.mounted;
|
||||
component.mounted = function() {
|
||||
current.call(component);
|
||||
cb();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* willUnmount hook. The callback will be called when the current component is
|
||||
* willUnmounted. Note that the component mounted method is called last.
|
||||
*/
|
||||
export function onWillUnmount(cb) {
|
||||
const component = Component._current;
|
||||
const current = component.willUnmount;
|
||||
component.willUnmount = function() {
|
||||
cb();
|
||||
current.call(component);
|
||||
};
|
||||
}
|
||||
@@ -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", {
|
||||
|
||||
@@ -254,7 +254,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};`);
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { Destination, RouterEnv } from "./Router";
|
||||
|
||||
type Props = Destination;
|
||||
|
||||
export class Link<Env extends RouterEnv> extends Component<Env, Props, {}> {
|
||||
export class Link<Env extends RouterEnv> extends Component<Env, Props> {
|
||||
static template = xml`
|
||||
<a t-att-class="{'router-link-active': isActive }"
|
||||
t-att-href="href"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ import { VNode } from "../vdom/index";
|
||||
|
||||
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;
|
||||
|
||||
+10
-13
@@ -1,5 +1,6 @@
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { QWeb } from "../src/qweb/index";
|
||||
import { useState } from "../src/hooks";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestFixture,
|
||||
@@ -36,7 +37,7 @@ afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -106,7 +107,7 @@ describe("animations", () => {
|
||||
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
state = useState({ hide: false });
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
@@ -148,7 +149,7 @@ describe("animations", () => {
|
||||
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { hide: false };
|
||||
state = useState({ hide: false });
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -269,7 +270,7 @@ describe("animations", () => {
|
||||
`<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
@@ -297,10 +298,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 +356,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;
|
||||
}
|
||||
|
||||
@@ -1239,6 +1239,30 @@ exports[`t-model directive basic use, on an input 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive basic use, on another key in component 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,on:{}};
|
||||
var vn2 = h('input', p2, c2);
|
||||
c1.push(vn2);
|
||||
p2.props = {value: context['some'].text};
|
||||
extra.handlers['input' + 2] = extra.handlers['input' + 2] || ((ev) => {context['some'].text = ev.target.value});
|
||||
p2.on['input'] = extra.handlers['input' + 2];
|
||||
let c3 = [], p3 = {key:3};
|
||||
var vn3 = h('span', p3, c3);
|
||||
c1.push(vn3);
|
||||
var _4 = context['some'].text;
|
||||
if (_4 || _4 === 0) {
|
||||
c3.push({text: _4});
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive on a select 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
|
||||
+147
-126
@@ -1,6 +1,7 @@
|
||||
import { Component, Env } from "../../src/component/component";
|
||||
import { QWeb } from "../../src/qweb/qweb";
|
||||
import { xml } from "../../src/tags";
|
||||
import { useState } from "../../src/hooks";
|
||||
import { EventBus } from "../../src/core/event_bus";
|
||||
import {
|
||||
makeDeferred,
|
||||
@@ -40,7 +41,7 @@ afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
function children(w: Widget): Widget[] {
|
||||
const childrenMap = w.__owl__.children;
|
||||
@@ -49,9 +50,9 @@ function children(w: Widget): Widget[] {
|
||||
|
||||
// Test components
|
||||
class Counter extends Widget {
|
||||
state = {
|
||||
state = useState({
|
||||
counter: 0
|
||||
};
|
||||
});
|
||||
|
||||
inc() {
|
||||
this.state.counter++;
|
||||
@@ -69,10 +70,9 @@ class WidgetA extends Widget {
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
describe("basic widget properties", () => {
|
||||
test("props and state are properly defined", async () => {
|
||||
test("props is properly defined", async () => {
|
||||
const widget = new Widget(env);
|
||||
expect(widget.props).toEqual({});
|
||||
expect(widget.state).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("has no el after creation", async () => {
|
||||
@@ -88,7 +88,7 @@ describe("basic widget properties", () => {
|
||||
|
||||
test("crashes if it cannot find a template", async () => {
|
||||
expect.assertions(1);
|
||||
class SomeWidget extends Component<any, any, any> {}
|
||||
class SomeWidget extends Component<any, any> {}
|
||||
const widget = new SomeWidget(env);
|
||||
try {
|
||||
await widget.mount(fixture);
|
||||
@@ -133,7 +133,7 @@ describe("basic widget properties", () => {
|
||||
test("changing state before first render does not trigger a render", async () => {
|
||||
let renderCalls = 0;
|
||||
class TestW extends Widget {
|
||||
state = { drinks: 1 };
|
||||
state = useState({ drinks: 1 });
|
||||
async willStart() {
|
||||
this.state.drinks++;
|
||||
}
|
||||
@@ -165,11 +165,11 @@ describe("basic widget properties", () => {
|
||||
test("reconciliation alg is not confused in some specific situation", async () => {
|
||||
// in this test, we set t-key to 4 because it was in conflict with the
|
||||
// template id corresponding to the first child.
|
||||
class Child extends Component<any, any, any> {
|
||||
class Child extends Component<any, any> {
|
||||
static template = xml`<span>child</span>`;
|
||||
}
|
||||
|
||||
class Parent extends Component<any, any, any> {
|
||||
class Parent extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child />
|
||||
@@ -288,7 +288,7 @@ describe("lifecycle hooks", () => {
|
||||
class ParentWidget extends Widget {
|
||||
static template = xml`<div><t t-if="state.flag"><t t-component="child"/></t></div>`;
|
||||
static components = { child: ChildWidget };
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
mounted() {
|
||||
steps.push("parent:mounted");
|
||||
}
|
||||
@@ -339,7 +339,7 @@ describe("lifecycle hooks", () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="child" n="state.n"/></div>`);
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: ChildWidget };
|
||||
state = { n: 1 };
|
||||
state = useState({ n: 1 });
|
||||
willPatch() {
|
||||
steps.push("parent:willPatch");
|
||||
}
|
||||
@@ -391,7 +391,7 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
}
|
||||
class ParentWidget extends Widget {
|
||||
state = { ok: false };
|
||||
state = useState({ ok: false });
|
||||
static components = { child: ChildWidget };
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
@@ -452,7 +452,7 @@ describe("lifecycle hooks", () => {
|
||||
</div>
|
||||
`;
|
||||
static components = { ChildWidget };
|
||||
state = { ok: true };
|
||||
state = useState({ ok: true });
|
||||
}
|
||||
|
||||
const widget = new ParentWidget(env);
|
||||
@@ -471,9 +471,6 @@ describe("lifecycle hooks", () => {
|
||||
willUnmount() {
|
||||
childUnmounted = true;
|
||||
}
|
||||
increment() {
|
||||
this.state += 1;
|
||||
}
|
||||
}
|
||||
|
||||
class ParentWidget extends Widget {
|
||||
@@ -485,7 +482,7 @@ describe("lifecycle hooks", () => {
|
||||
</div>
|
||||
`;
|
||||
static components = { ChildWidget };
|
||||
state = { n: 0, flag: true };
|
||||
state = useState({ n: 0, flag: true });
|
||||
increment() {
|
||||
this.state.n += 1;
|
||||
}
|
||||
@@ -567,7 +564,7 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
}
|
||||
class Parent extends Widget {
|
||||
state = { n: 1 };
|
||||
state = useState({ n: 1 });
|
||||
static components = { Child: HookWidget };
|
||||
}
|
||||
env.qweb.addTemplate("HookWidget", '<span><t t-esc="props.n"/></span>');
|
||||
@@ -586,7 +583,7 @@ describe("lifecycle hooks", () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = { a: 1 };
|
||||
state = useState({ a: 1 });
|
||||
|
||||
patched() {
|
||||
n++;
|
||||
@@ -615,7 +612,7 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static template = xml`<div><Child a="state.a"/></div>`;
|
||||
state = { a: 1 };
|
||||
state = useState({ a: 1 });
|
||||
static components = { Child: TestWidget };
|
||||
}
|
||||
|
||||
@@ -632,7 +629,7 @@ describe("lifecycle hooks", () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = { a: 1 };
|
||||
state = useState({ a: 1 });
|
||||
|
||||
patched() {
|
||||
n++;
|
||||
@@ -657,7 +654,7 @@ describe("lifecycle hooks", () => {
|
||||
class Parent extends Widget {
|
||||
static template = xml`<div><Child val="state.val"/></div>`;
|
||||
static components = { Child: TestWidget };
|
||||
state = { val: 42 };
|
||||
state = useState({ val: 42 });
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
@@ -694,7 +691,7 @@ describe("lifecycle hooks", () => {
|
||||
</div>
|
||||
`;
|
||||
static components = { child: ChildWidget };
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -725,7 +722,7 @@ describe("lifecycle hooks", () => {
|
||||
</div>
|
||||
`;
|
||||
static components = { child: ChildWidget };
|
||||
state = { n: 1 };
|
||||
state = useState({ n: 1 });
|
||||
willPatch() {
|
||||
steps.push("parent:willPatch");
|
||||
return "leffe";
|
||||
@@ -777,7 +774,7 @@ describe("lifecycle hooks", () => {
|
||||
</div>
|
||||
`;
|
||||
static components = { ChildWidget };
|
||||
state = { n: 1, flag: true };
|
||||
state = useState({ n: 1, flag: true });
|
||||
}
|
||||
|
||||
const widget = new ParentWidget(env);
|
||||
@@ -855,14 +852,14 @@ describe("destroy method", () => {
|
||||
});
|
||||
|
||||
test("destroying a widget before being mounted", async () => {
|
||||
class GrandChild extends Component<any, any, any> {
|
||||
class GrandChild extends Component<any, any> {
|
||||
static template = xml`
|
||||
<span>
|
||||
<t t-esc="props.val.val"/>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
class Child extends Component<any, any, any> {
|
||||
class Child extends Component<any, any> {
|
||||
static template = xml`
|
||||
<span>
|
||||
<GrandChild t-if="state.flag" val="something"/>
|
||||
@@ -870,7 +867,7 @@ describe("destroy method", () => {
|
||||
</span>
|
||||
`;
|
||||
static components = { GrandChild };
|
||||
state = { val: 33, flag: false };
|
||||
state = useState({ val: 33, flag: false });
|
||||
doSomething() {
|
||||
this.state.val = 12;
|
||||
this.state.flag = true;
|
||||
@@ -880,14 +877,14 @@ describe("destroy method", () => {
|
||||
return { val: this.state.val };
|
||||
}
|
||||
}
|
||||
class Parent extends Component<any, any, any> {
|
||||
class Parent extends Component<any, any> {
|
||||
static template = xml`
|
||||
<div t-on-some-event="doStuff">
|
||||
<Child />
|
||||
</div>
|
||||
`;
|
||||
static components = { Child };
|
||||
state = { p: 1 };
|
||||
state = useState({ p: 1 });
|
||||
doStuff() {
|
||||
this.state.p = 2;
|
||||
}
|
||||
@@ -921,17 +918,17 @@ describe("composition", () => {
|
||||
});
|
||||
|
||||
test("can use dynamic components (the class) if given", async () => {
|
||||
class A extends Component<any, any, any> {
|
||||
class A extends Component<any, any> {
|
||||
static template = xml`<span>child a</span>`;
|
||||
}
|
||||
class B extends Component<any, any, any> {
|
||||
class B extends Component<any, any> {
|
||||
static template = xml`<span>child b</span>`;
|
||||
}
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
|
||||
state = {
|
||||
state = useState({
|
||||
child: "a"
|
||||
};
|
||||
});
|
||||
get myComponent() {
|
||||
return this.state.child === "a" ? A : B;
|
||||
}
|
||||
@@ -1010,7 +1007,7 @@ describe("composition", () => {
|
||||
</div>
|
||||
`;
|
||||
static components = { Widget };
|
||||
state = { list: <any>[] };
|
||||
state = useState({ list: <any>[] });
|
||||
willPatch() {
|
||||
expect(this.refs.child).toBeUndefined();
|
||||
}
|
||||
@@ -1037,7 +1034,7 @@ describe("composition", () => {
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
static components = { Widget };
|
||||
state = { child1: true, child2: false };
|
||||
state = useState({ child1: true, child2: false });
|
||||
count = 0;
|
||||
mounted() {
|
||||
expect(this.refs.child1).toBeDefined();
|
||||
@@ -1098,7 +1095,7 @@ describe("composition", () => {
|
||||
</div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
state = { items: [1, 2, 3] };
|
||||
state = useState({ items: [1, 2, 3] });
|
||||
static components = { Child: Widget };
|
||||
}
|
||||
const parent = new ParentWidget(env);
|
||||
@@ -1144,7 +1141,7 @@ describe("composition", () => {
|
||||
test("sub components are destroyed if no longer in dom, then recreated", async () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-if="state.ok"><Counter /></t></div>`);
|
||||
class ParentWidget extends Widget {
|
||||
state = { ok: true };
|
||||
state = useState({ ok: true });
|
||||
static components = { Counter };
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
@@ -1168,7 +1165,7 @@ describe("composition", () => {
|
||||
`<div><t t-if="state.ok"><Counter t-keepalive="1"/></t></div>`
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
state = { ok: true };
|
||||
state = useState({ ok: true });
|
||||
static components = { Counter };
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
@@ -1198,7 +1195,7 @@ describe("composition", () => {
|
||||
);
|
||||
class InputWidget extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
state = { ok: true };
|
||||
state = useState({ ok: true });
|
||||
static components = { InputWidget };
|
||||
}
|
||||
env.qweb.addTemplate("InputWidget", "<input/>");
|
||||
@@ -1229,7 +1226,7 @@ describe("composition", () => {
|
||||
</templates>`);
|
||||
class ChildWidget extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
state = { ok: true };
|
||||
state = useState({ ok: true });
|
||||
static components = { ChildWidget };
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
@@ -1266,9 +1263,9 @@ describe("composition", () => {
|
||||
</div>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
state = {
|
||||
state = useState({
|
||||
numbers: [1, 2, 3]
|
||||
};
|
||||
});
|
||||
static components = { ChildWidget };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
@@ -1288,9 +1285,10 @@ describe("composition", () => {
|
||||
let n = 1;
|
||||
env.qweb.addTemplate("ChildWidget", `<span><t t-esc="state.n"/></span>`);
|
||||
class ChildWidget extends Widget {
|
||||
state: any;
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
this.state = { n };
|
||||
this.state = useState({ n });
|
||||
n++;
|
||||
}
|
||||
}
|
||||
@@ -1305,9 +1303,9 @@ describe("composition", () => {
|
||||
);
|
||||
class Parent extends Widget {
|
||||
static template = "parent";
|
||||
state = {
|
||||
state = useState({
|
||||
numbers: [1, 2, 3]
|
||||
};
|
||||
});
|
||||
static components = { ChildWidget };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
@@ -1341,7 +1339,7 @@ describe("composition", () => {
|
||||
);
|
||||
|
||||
class Parent extends Widget {
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
static components = { ChildWidget };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
@@ -1380,7 +1378,7 @@ describe("composition", () => {
|
||||
class SubWidget extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { SubWidget };
|
||||
state = { blips: [{ a: "a", id: 1 }, { b: "b", id: 2 }, { c: "c", id: 4 }] };
|
||||
state = useState({ blips: [{ a: "a", id: 1 }, { b: "b", id: 2 }, { c: "c", id: 4 }] });
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
@@ -1412,7 +1410,7 @@ describe("composition", () => {
|
||||
class SubWidget extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { SubWidget };
|
||||
state = { blips: [{ a: "a", id: 1 }] };
|
||||
state = useState({ blips: [{ a: "a", id: 1 }] });
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
@@ -1423,7 +1421,7 @@ describe("composition", () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="{{state.widget}}"/></div>`);
|
||||
class ParentWidget extends Widget {
|
||||
static components = { WidgetB };
|
||||
state = { widget: "WidgetB" };
|
||||
state = useState({ widget: "WidgetB" });
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -1435,7 +1433,7 @@ describe("composition", () => {
|
||||
env.qweb.addTemplate("ParentWidget", `<div><t t-component="Widget{{state.widget}}"/></div>`);
|
||||
class ParentWidget extends Widget {
|
||||
static components = { WidgetB };
|
||||
state = { widget: "B" };
|
||||
state = useState({ widget: "B" });
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -1451,12 +1449,12 @@ describe("props evaluation ", () => {
|
||||
state: { someval: number };
|
||||
constructor(parent: Parent, props: { value: number }) {
|
||||
super(parent);
|
||||
this.state = { someval: props.value };
|
||||
this.state = useState({ someval: props.value });
|
||||
}
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static components = { child: Child };
|
||||
state = { val: 42 };
|
||||
state = useState({ val: 42 });
|
||||
}
|
||||
|
||||
env.qweb.addTemplate("Child", `<span><t t-esc="state.someval"/></span>`);
|
||||
@@ -1538,7 +1536,7 @@ describe("class and style attributes with t-component", () => {
|
||||
class Child extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: Child };
|
||||
state = { a: true, b: false };
|
||||
state = useState({ a: true, b: false });
|
||||
}
|
||||
env.qweb.addTemplate("Child", `<div class="c"/>`);
|
||||
const widget = new ParentWidget(env);
|
||||
@@ -1579,11 +1577,11 @@ describe("class and style attributes with t-component", () => {
|
||||
</templates>`);
|
||||
|
||||
class Child extends Widget {
|
||||
state = { d: true };
|
||||
state = useState({ d: true });
|
||||
}
|
||||
class ParentWidget extends Widget {
|
||||
static components = { Child };
|
||||
state = { b: true };
|
||||
state = useState({ b: true });
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -1620,11 +1618,11 @@ describe("class and style attributes with t-component", () => {
|
||||
</templates>`);
|
||||
|
||||
class Child extends Widget {
|
||||
state = { d: true };
|
||||
state = useState({ d: true });
|
||||
}
|
||||
class ParentWidget extends Widget {
|
||||
static components = { Child };
|
||||
state = { b: true };
|
||||
state = useState({ b: true });
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -1658,7 +1656,7 @@ describe("class and style attributes with t-component", () => {
|
||||
</templates>`);
|
||||
|
||||
class App extends Widget {
|
||||
state = { c: true };
|
||||
state = useState({ c: true });
|
||||
mounted() {
|
||||
this.el!.classList.add("user");
|
||||
}
|
||||
@@ -1701,7 +1699,7 @@ describe("class and style attributes with t-component", () => {
|
||||
);
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: Widget };
|
||||
state = { style: "font-size: 20px" };
|
||||
state = useState({ style: "font-size: 20px" });
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -1977,7 +1975,7 @@ describe("other directives with t-component", () => {
|
||||
class Child extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: Child };
|
||||
state = { flag: true };
|
||||
state = useState({ flag: true });
|
||||
}
|
||||
env.qweb.addTemplate("Child", "<span>hey</span>");
|
||||
|
||||
@@ -2007,7 +2005,7 @@ describe("other directives with t-component", () => {
|
||||
class Child extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: Child };
|
||||
state = { flag: true };
|
||||
state = useState({ flag: true });
|
||||
}
|
||||
env.qweb.addTemplate("Child", "<span>hey</span>");
|
||||
|
||||
@@ -2033,7 +2031,7 @@ describe("other directives with t-component", () => {
|
||||
class Child extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: Child };
|
||||
state = { flag: true };
|
||||
state = useState({ flag: true });
|
||||
}
|
||||
env.qweb.addTemplate("Child", "<span>hey</span>");
|
||||
|
||||
@@ -2059,7 +2057,7 @@ describe("other directives with t-component", () => {
|
||||
class Child extends Widget {}
|
||||
class ParentWidget extends Widget {
|
||||
static components = { child: Child };
|
||||
state = { flag: true };
|
||||
state = useState({ flag: true });
|
||||
}
|
||||
env.qweb.addTemplate("Child", "<span>hey</span>");
|
||||
|
||||
@@ -2127,7 +2125,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { child: Child };
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
}
|
||||
|
||||
env.qweb.addTemplate("Child", `<span>abc<t t-if="props.flag">def</t></span>`);
|
||||
@@ -2148,7 +2146,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { child: Child };
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
}
|
||||
|
||||
env.qweb.addTemplate("Child", `<span>abc<t t-if="props.flag">def</t></span>`);
|
||||
@@ -2243,7 +2241,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
class C extends TestWidget {
|
||||
static components = { D, E, F };
|
||||
name = "C";
|
||||
state = { flag: true };
|
||||
state = useState({ flag: true });
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
@@ -2315,7 +2313,7 @@ describe("random stuff/miscellaneous", () => {
|
||||
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 };
|
||||
state = useState({ n: 42 });
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
@@ -2360,7 +2358,7 @@ describe("async rendering", () => {
|
||||
}
|
||||
class W extends Widget {
|
||||
static components = { Child };
|
||||
state = { val: 1 };
|
||||
state = useState({ val: 1 });
|
||||
}
|
||||
|
||||
const w = new W(env);
|
||||
@@ -2406,7 +2404,7 @@ describe("async rendering", () => {
|
||||
);
|
||||
class Parent extends Widget {
|
||||
static components = { ChildA, ChildB };
|
||||
state = { flagA: false, flagB: false };
|
||||
state = useState({ flagA: false, flagB: false });
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
@@ -2453,7 +2451,7 @@ describe("async rendering", () => {
|
||||
);
|
||||
class Parent extends Widget {
|
||||
static components = { ChildA, ChildB };
|
||||
state = { valA: 1, valB: 2, flagB: false };
|
||||
state = useState({ valA: 1, valB: 2, flagB: false });
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
@@ -2538,7 +2536,7 @@ describe("async rendering", () => {
|
||||
);
|
||||
class Parent extends Widget {
|
||||
static components = { Child };
|
||||
state = { flag: true, val: "Framboise Lindemans" };
|
||||
state = useState({ flag: true, val: "Framboise Lindemans" });
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
@@ -2588,7 +2586,7 @@ describe("async rendering", () => {
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static components = { ChildA, ChildB };
|
||||
state = { valA: 1, valB: 2, flag: false };
|
||||
state = useState({ valA: 1, valB: 2, flag: false });
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
await parent.mount(fixture);
|
||||
@@ -2627,7 +2625,7 @@ describe("async rendering", () => {
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static components = { Child, AsyncChild };
|
||||
state = { val: 0 };
|
||||
state = useState({ val: 0 });
|
||||
|
||||
updateApp() {
|
||||
this.state.val++;
|
||||
@@ -2676,7 +2674,7 @@ describe("async rendering", () => {
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static components = { Child, AsyncChild };
|
||||
state = { val: 0 };
|
||||
state = useState({ val: 0 });
|
||||
|
||||
updateApp() {
|
||||
this.state.val++;
|
||||
@@ -2720,7 +2718,7 @@ describe("async rendering", () => {
|
||||
|
||||
let def;
|
||||
class Child extends Widget {
|
||||
state = { val: 0 };
|
||||
state = useState({ val: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.val++;
|
||||
@@ -2733,7 +2731,7 @@ describe("async rendering", () => {
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static components = { Child, AsyncChild };
|
||||
state = { val: 0 };
|
||||
state = useState({ val: 0 });
|
||||
|
||||
updateApp() {
|
||||
this.state.val++;
|
||||
@@ -2909,7 +2907,7 @@ describe("widget and observable state", () => {
|
||||
test("widget is rerendered when its state is changed", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><t t-esc="state.drink"/></div>`);
|
||||
class TestWidget extends Widget {
|
||||
state = { drink: "water" };
|
||||
state = useState({ drink: "water" });
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
@@ -2934,7 +2932,7 @@ describe("widget and observable state", () => {
|
||||
}
|
||||
}
|
||||
class Parent extends Widget {
|
||||
state = { obj: { coffee: 1 } };
|
||||
state = useState({ obj: { coffee: 1 } });
|
||||
static components = { Child };
|
||||
}
|
||||
const parent = new Parent(env);
|
||||
@@ -2986,7 +2984,7 @@ describe("t-mounted directive", () => {
|
||||
test("combined with a t-if", async () => {
|
||||
env.qweb.addTemplate("TestWidget", `<div><input t-if="state.flag" t-mounted="f"/></div>`);
|
||||
class TestWidget extends Widget {
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
f() {}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
@@ -3103,7 +3101,7 @@ describe("t-slot directive", () => {
|
||||
class Dialog extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Dialog };
|
||||
state = { val: 0 };
|
||||
state = useState({ val: 0 });
|
||||
doSomething() {
|
||||
this.state.val++;
|
||||
}
|
||||
@@ -3140,7 +3138,7 @@ describe("t-slot directive", () => {
|
||||
class Link extends Widget {}
|
||||
|
||||
class App extends Widget {
|
||||
state = { users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] };
|
||||
state = useState({ users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] });
|
||||
static components = { Link };
|
||||
}
|
||||
|
||||
@@ -3179,7 +3177,7 @@ describe("t-slot directive", () => {
|
||||
class Link extends Widget {}
|
||||
|
||||
class App extends Widget {
|
||||
state = { users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] };
|
||||
state = useState({ users: [{ id: 1, name: "Aaron" }, { id: 2, name: "David" }] });
|
||||
static components = { Link };
|
||||
}
|
||||
|
||||
@@ -3216,7 +3214,7 @@ describe("t-slot directive", () => {
|
||||
class Link extends Widget {}
|
||||
|
||||
class App extends Widget {
|
||||
state = { user: { id: 1, name: "Aaron" } };
|
||||
state = useState({ user: { id: 1, name: "Aaron" } });
|
||||
static components = { Link };
|
||||
}
|
||||
|
||||
@@ -3249,7 +3247,7 @@ describe("t-slot directive", () => {
|
||||
class Dialog extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Dialog };
|
||||
state = { val: 0 };
|
||||
state = useState({ val: 0 });
|
||||
doSomething() {
|
||||
this.state.val++;
|
||||
}
|
||||
@@ -3462,7 +3460,7 @@ describe("t-slot directive", () => {
|
||||
class GenericComponent extends Widget {}
|
||||
class App extends Widget {
|
||||
static components = { GenericComponent, SomeComponent };
|
||||
state = { val: 4 };
|
||||
state = useState({ val: 4 });
|
||||
|
||||
inc() {
|
||||
this.state.val++;
|
||||
@@ -3478,14 +3476,14 @@ describe("t-slot directive", () => {
|
||||
});
|
||||
|
||||
test("slots and wrapper components", async () => {
|
||||
class Link extends Component<any, any, any> {
|
||||
class Link extends Component<any, any> {
|
||||
static template = xml`
|
||||
<a href="abc">
|
||||
<t t-slot="default"/>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
class A extends Component<any, any, any> {
|
||||
class A extends Component<any, any> {
|
||||
static template = xml`<Link>hey</Link>`;
|
||||
static components = { Link: Link };
|
||||
}
|
||||
@@ -3502,12 +3500,12 @@ describe("t-model directive", () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="SomeComponent">
|
||||
<input t-model="text"/>
|
||||
<input t-model="state.text"/>
|
||||
<span><t t-esc="state.text"/></span>
|
||||
</div>
|
||||
</templates>`);
|
||||
class SomeComponent extends Widget {
|
||||
state = { text: "" };
|
||||
state = useState({ text: "" });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3521,11 +3519,34 @@ describe("t-model directive", () => {
|
||||
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("basic use, on another key in component", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="SomeComponent">
|
||||
<input t-model="some.text"/>
|
||||
<span><t t-esc="some.text"/></span>
|
||||
</div>
|
||||
</templates>`);
|
||||
class SomeComponent extends Widget {
|
||||
some = useState({ text: "" });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
||||
|
||||
const input = fixture.querySelector("input")!;
|
||||
await editInput(input, "test");
|
||||
expect(comp.some.text).toBe("test");
|
||||
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
||||
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("on an input, type=checkbox", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="SomeComponent">
|
||||
<input type="checkbox" t-model="flag"/>
|
||||
<input type="checkbox" t-model="state.flag"/>
|
||||
<span>
|
||||
<t t-if="state.flag">yes</t>
|
||||
<t t-else="1">no</t>
|
||||
@@ -3533,7 +3554,7 @@ describe("t-model directive", () => {
|
||||
</div>
|
||||
</templates>`);
|
||||
class SomeComponent extends Widget {
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3556,12 +3577,12 @@ describe("t-model directive", () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="SomeComponent">
|
||||
<textarea t-model="text"/>
|
||||
<textarea t-model="state.text"/>
|
||||
<span><t t-esc="state.text"/></span>
|
||||
</div>
|
||||
</templates>`);
|
||||
class SomeComponent extends Widget {
|
||||
state = { text: "" };
|
||||
state = useState({ text: "" });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3578,13 +3599,13 @@ describe("t-model directive", () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="SomeComponent">
|
||||
<input type="radio" id="one" value="One" t-model="choice"/>
|
||||
<input type="radio" id="two" value="Two" t-model="choice"/>
|
||||
<input type="radio" id="one" value="One" t-model="state.choice"/>
|
||||
<input type="radio" id="two" value="Two" t-model="state.choice"/>
|
||||
<span>Choice: <t t-esc="state.choice"/></span>
|
||||
</div>
|
||||
</templates>`);
|
||||
class SomeComponent extends Widget {
|
||||
state = { choice: "" };
|
||||
state = useState({ choice: "" });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3615,7 +3636,7 @@ describe("t-model directive", () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="SomeComponent">
|
||||
<select t-model="color">
|
||||
<select t-model="state.color">
|
||||
<option value="">Please select one</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
@@ -3624,7 +3645,7 @@ describe("t-model directive", () => {
|
||||
</div>
|
||||
</templates>`);
|
||||
class SomeComponent extends Widget {
|
||||
state = { color: "" };
|
||||
state = useState({ color: "" });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3650,14 +3671,14 @@ describe("t-model directive", () => {
|
||||
class SomeComponent extends Widget {
|
||||
static template = xml`
|
||||
<div>
|
||||
<select t-model="color">
|
||||
<select t-model="state.color">
|
||||
<option value="">Please select one</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
state = { color: "red" };
|
||||
state = useState({ color: "red" });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3669,11 +3690,11 @@ describe("t-model directive", () => {
|
||||
class SomeComponent extends Widget {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input t-model="something.text"/>
|
||||
<input t-model="state.something.text"/>
|
||||
<span><t t-esc="state.something.text"/></span>
|
||||
</div>
|
||||
`;
|
||||
state = { something: { text: "" } };
|
||||
state = useState({ something: { text: "" } });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3691,11 +3712,11 @@ describe("t-model directive", () => {
|
||||
class SomeComponent extends Widget {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input t-model.lazy="text"/>
|
||||
<input t-model.lazy="state.text"/>
|
||||
<span><t t-esc="state.text"/></span>
|
||||
</div>
|
||||
`;
|
||||
state = { text: "" };
|
||||
state = useState({ text: "" });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3719,11 +3740,11 @@ describe("t-model directive", () => {
|
||||
class SomeComponent extends Widget {
|
||||
static template = xml`
|
||||
<div t-name="SomeComponent">
|
||||
<input t-model.trim="text"/>
|
||||
<input t-model.trim="state.text"/>
|
||||
<span><t t-esc="state.text"/></span>
|
||||
</div>
|
||||
`;
|
||||
state = { text: "" };
|
||||
state = useState({ text: "" });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3738,11 +3759,11 @@ describe("t-model directive", () => {
|
||||
class SomeComponent extends Widget {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input t-model.number="number"/>
|
||||
<input t-model.number="state.number"/>
|
||||
<span><t t-esc="state.number"/></span>
|
||||
</div>
|
||||
`;
|
||||
state = { number: 0 };
|
||||
state = useState({ number: 0 });
|
||||
}
|
||||
const comp = new SomeComponent(env);
|
||||
await comp.mount(fixture);
|
||||
@@ -3823,14 +3844,14 @@ describe("component error handling (catchError)", () => {
|
||||
env.qweb.on("error", null, handler);
|
||||
class ErrorComponent extends Widget {}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Widget {
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
@@ -3862,7 +3883,7 @@ describe("component error handling (catchError)", () => {
|
||||
</templates>`);
|
||||
class ErrorComponent extends Widget {}
|
||||
class App extends Widget {
|
||||
state = { flag: false };
|
||||
state = useState({ flag: false });
|
||||
static components = { ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
@@ -3899,7 +3920,7 @@ describe("component error handling (catchError)", () => {
|
||||
</templates>`);
|
||||
class ErrorComponent extends Widget {}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
@@ -3943,7 +3964,7 @@ describe("component error handling (catchError)", () => {
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
@@ -3986,7 +4007,7 @@ describe("component error handling (catchError)", () => {
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
@@ -4025,7 +4046,7 @@ describe("component error handling (catchError)", () => {
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
@@ -4060,14 +4081,14 @@ describe("component error handling (catchError)", () => {
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Widget {
|
||||
state = { error: false };
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Widget {
|
||||
state = { message: "abc" };
|
||||
state = useState({ message: "abc" });
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App(env);
|
||||
@@ -4109,7 +4130,7 @@ describe("top level sub widgets", () => {
|
||||
<span t-name="Child"><button t-on-click="inc">click</button>child<t t-esc="state.val"/></span>
|
||||
</templates>`);
|
||||
class Child extends Widget {
|
||||
state = { val: 1 };
|
||||
state = useState({ val: 1 });
|
||||
inc() {
|
||||
this.state.val++;
|
||||
}
|
||||
@@ -4169,7 +4190,7 @@ describe("top level sub widgets", () => {
|
||||
<t t-if="!state.flag"><OtherChild /></t>
|
||||
</t>
|
||||
`;
|
||||
state = { flag: true };
|
||||
state = useState({ flag: true });
|
||||
static components = { Child, OtherChild };
|
||||
}
|
||||
let parent = new Parent(env);
|
||||
@@ -4240,7 +4261,7 @@ describe("unmounting and remounting", () => {
|
||||
static template = xml`
|
||||
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
|
||||
`;
|
||||
state = { n: 2 };
|
||||
state = useState({ n: 2 });
|
||||
__render(f) {
|
||||
steps.push("render");
|
||||
return super.__render(f);
|
||||
@@ -4264,7 +4285,7 @@ describe("unmounting and remounting", () => {
|
||||
</div>
|
||||
`;
|
||||
static components = { Child };
|
||||
state = { val: 1, flag: true };
|
||||
state = useState({ val: 1, flag: true });
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
@@ -4282,7 +4303,7 @@ describe("unmounting and remounting", () => {
|
||||
static template = xml`
|
||||
<div><t t-esc="state.val"/></div>
|
||||
`;
|
||||
state = { val: 1 };
|
||||
state = useState({ val: 1 });
|
||||
willUnmount() {
|
||||
this.state.val = 3;
|
||||
}
|
||||
@@ -4342,7 +4363,7 @@ describe("dynamic root nodes", () => {
|
||||
<t t-if="!state.flag"><div>abc</div></t>
|
||||
</t>
|
||||
`;
|
||||
state = { flag: true };
|
||||
state = useState({ flag: true });
|
||||
}
|
||||
|
||||
const widget = new TestWidget(env);
|
||||
@@ -4370,7 +4391,7 @@ describe("dynamic root nodes", () => {
|
||||
</t>
|
||||
`;
|
||||
static components = { ChildA, ChildB };
|
||||
state = { flag: true };
|
||||
state = useState({ flag: true });
|
||||
}
|
||||
|
||||
const widget = new TestWidget(env);
|
||||
|
||||
@@ -22,7 +22,7 @@ afterEach(() => {
|
||||
QWeb.dev = dev;
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any, any> {}
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { useState, onMounted, onWillUnmount } 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(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"
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1333,5 +1333,5 @@ describe("properly support svg", () => {
|
||||
expect(renderToString(qweb, "test")).toBe(
|
||||
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("Link component", () => {
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { Link: Link };
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("Link component", () => {
|
||||
</div>
|
||||
</templates>
|
||||
`);
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { Link: Link };
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ describe("RouteComponent", () => {
|
||||
<span t-name="Users">Users</span>
|
||||
</templates>
|
||||
`);
|
||||
class About extends Component<any, any, any> {}
|
||||
class Users extends Component<any, any, any> {}
|
||||
class App extends Component<any, any, any> {
|
||||
class About extends Component<any, any> {}
|
||||
class Users extends Component<any, any> {}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ describe("RouteComponent", () => {
|
||||
<span t-name="Book">Book <t t-esc="props.title"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Book extends Component<any, any, any> {}
|
||||
class App extends Component<any, any, any> {
|
||||
class Book extends Component<any, any> {}
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
@@ -86,12 +86,12 @@ describe("RouteComponent", () => {
|
||||
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
|
||||
</templates>
|
||||
`);
|
||||
class Book extends Component<any, any, any> {
|
||||
class Book extends Component<any, any> {
|
||||
get incVal() {
|
||||
return this.props.val + 1;
|
||||
}
|
||||
}
|
||||
class App extends Component<any, any, any> {
|
||||
class App extends Component<any, any> {
|
||||
static components = { RouteComponent };
|
||||
}
|
||||
|
||||
|
||||
@@ -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++;
|
||||
@@ -703,7 +704,7 @@ describe("connecting a component to store", () => {
|
||||
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) {
|
||||
@@ -782,7 +783,7 @@ describe("connecting a component to store", () => {
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -936,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 {
|
||||
@@ -945,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) {
|
||||
@@ -956,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 };
|
||||
}
|
||||
@@ -1018,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 {
|
||||
@@ -1062,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
|
||||
@@ -1143,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
|
||||
|
||||
+5
-7
@@ -206,17 +206,15 @@ describe("snabbdom", function() {
|
||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||
|
||||
// verify that svg tag automatically gets svg namespace
|
||||
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);
|
||||
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;
|
||||
elm = patch(vnode0, vnode).elm;
|
||||
expect(elm.namespaceURI).toBe(SVGNamespace);
|
||||
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
|
||||
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
|
||||
|
||||
});
|
||||
|
||||
test("receives classes in selector", function() {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
|
||||
|
||||
const useState = owl.hooks.useState;
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
state = { counter: 0 };
|
||||
state = useState({ counter: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
@@ -32,7 +33,7 @@ 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 });
|
||||
|
||||
mounted() {
|
||||
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
import { SAMPLES } from "./samples.js";
|
||||
|
||||
const useState = owl.hooks.useState;
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants, helpers, utils
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -156,9 +156,9 @@ 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 = {};
|
||||
@@ -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,7 +259,7 @@ 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);
|
||||
|
||||
+111
-41
@@ -1,7 +1,8 @@
|
||||
const COMPONENTS = `// In this example, we show how components can be defined and created.
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class Greeter extends owl.Component {
|
||||
state = { word: 'Hello' };
|
||||
class Greeter extends Component {
|
||||
state = useState({ word: 'Hello' });
|
||||
|
||||
toggle() {
|
||||
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
|
||||
@@ -9,9 +10,9 @@ class Greeter extends owl.Component {
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
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,69 @@ 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 ENTER_KEY = 13;
|
||||
const ESC_KEY = 27;
|
||||
@@ -335,8 +395,8 @@ function makeStore() {
|
||||
//------------------------------------------------------------------------------
|
||||
// TodoItem
|
||||
//------------------------------------------------------------------------------
|
||||
class TodoItem extends owl.Component {
|
||||
state = { isEditing: false };
|
||||
class TodoItem extends Component {
|
||||
state = useState({ isEditing: false });
|
||||
|
||||
removeTodo() {
|
||||
this.env.store.dispatch("removeTodo", this.props.id);
|
||||
@@ -389,7 +449,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 +1112,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 +1131,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 +1229,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 +1239,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 +1312,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 +1334,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 +1355,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,18 +1379,19 @@ 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;
|
||||
|
||||
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;
|
||||
|
||||
@@ -1366,7 +1430,7 @@ class Window extends owl.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class WindowManager extends owl.Component {
|
||||
class WindowManager extends Component {
|
||||
static components = { Window };
|
||||
windows = [];
|
||||
nextId = 1;
|
||||
@@ -1406,7 +1470,7 @@ class WindowManager extends owl.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.Component {
|
||||
class App extends Component {
|
||||
static components = { WindowManager };
|
||||
|
||||
addWindow(name) {
|
||||
@@ -1569,6 +1633,12 @@ export const SAMPLES = [
|
||||
xml: LIFECYCLE_DEMO_XML,
|
||||
css: LIFECYCLE_CSS
|
||||
},
|
||||
{
|
||||
description: "Hooks",
|
||||
code: HOOKS_DEMO,
|
||||
xml: HOOKS_DEMO_XML,
|
||||
css: HOOKS_CSS
|
||||
},
|
||||
{
|
||||
description: "Todo List App (with store)",
|
||||
code: TODO_APP_STORE,
|
||||
|
||||
Reference in New Issue
Block a user