[IMP] components: add hooks mechanism

part of #194
This commit is contained in:
Géry Debongnie
2019-09-24 14:06:53 +02:00
parent 4513e3f31c
commit 5335fb8fba
28 changed files with 781 additions and 358 deletions
+4 -1
View File
@@ -20,6 +20,7 @@ Here is a short example to illustrate interactive components:
```javascript ```javascript
import { Component, QWeb } from 'owl' import { Component, QWeb } from 'owl'
import { xml } from 'owl/tags' import { xml } from 'owl/tags'
import { useState } from 'owl/hooks'
class Counter extends Component { class Counter extends Component {
static template = xml` static template = xml`
@@ -27,7 +28,7 @@ class Counter extends Component {
Click Me! [<t t-esc="state.value"/>] Click Me! [<t t-esc="state.value"/>]
</button>`; </button>`;
state = { value: 0 }; state = useState({ value: 0 });
increment() { increment() {
this.state.value++; this.state.value++;
@@ -48,6 +49,8 @@ const app = new App({ qweb: new QWeb() });
app.mount(document.body); app.mount(document.body);
``` ```
Note that the counter component is made reactive with the `useState` hook.
More interesting examples can be found on the More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application. [playground](https://odoo.github.io/owl/playground) application.
+51 -39
View File
@@ -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 Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way. of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
OWL components observe their states, and rerender themselves whenever it is
changed. This is done by an [observer](observer.md).
## Example ## Example
Let us have a look at a simple component: Let us have a look at a simple component:
```javascript ```javascript
const { useState } = owl.hooks;
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
state = { value: 0 }; state = useState({ value: 0 });
increment() { increment() {
this.state.value++; this.state.value++;
@@ -74,9 +74,27 @@ latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the This example show how a component should be defined: it simply subclasses the
Component class. If no static `template` key is defined, then Component class. If no static `template` key is defined, then
Owl will use the component's name as template name. Here, Owl will use the component's name as template name. Here,
a state object is defined. It is not mandatory to use the state object, but it a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
is certainly encouraged. The state object is [observed](observer.md), and any [observed](observer.md), and any change to it will cause a rerendering.
change to it will cause a rerendering.
## 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 ## 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. - **`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 - **`props`** (Object): this is an object given (in the constructor) by the parent
to configure the component. It can be dynamically changed later by the parent, to configure the component. It can be dynamically changed later by the parent,
in some case. Note that `props` are owned by the parent, not by the component. in some case. Note that `props` are owned by the parent, not by the component.
@@ -236,7 +250,7 @@ component.
```javascript ```javascript
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
this.state = {someValue: true}; this.state = useState({someValue: true});
this.template = 'mytemplate'; this.template = 'mytemplate';
} }
``` ```
@@ -246,7 +260,7 @@ implemented in most cases:
```javascript ```javascript
class ClickCounter extends owl.Component { 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 interface. Therefore, some care should be made to make this method as
fast as possible. fast as possible.
After the `willStart` method is completed, the state will be observed with a
new `Observer`. Then, the component will be rendered by `QWeb`.
#### `mounted()` #### `mounted()`
`mounted` is called each time a component is attached to the `mounted` is called each time a component is attached to the
@@ -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 mounted method will be called recursively on each of its children. First,
the parent, then all its children. the parent, then all its children.
Note that the state is now observed. It is however allowed (but not encouraged) It is allowed (but not encouraged) to modify the state in the `mounted` hook.
to modify the state in the `mounted` hook. Doing so will cause a rerender, Doing so will cause a rerender, which will not be perceptible by the user, but
which will not be perceptible by the user, but will slightly slow down the will slightly slow down the component.
component.
#### `willUpdateProps(nextProps)` #### `willUpdateProps(nextProps)`
@@ -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 information from the DOM. For example, the current position of the
scrollbar. scrollbar.
Note that modifying the state object is not allowed here. This method is called just Note that modifying the state is not allowed here. This method is called just
before an actual DOM patch, and is only intended to be used to save some local before an actual DOM patch, and is only intended to be used to save some local
DOM state. Also, it will not be called if the component is not in the DOM (this can DOM state. Also, it will not be called if the component is not in the DOM (this can
happen with components with `t-keepalive`). happen with components with `t-keepalive`).
@@ -397,9 +407,9 @@ easy to test a component.
Updating the environment is not as simple as changing a component's state: its Updating the environment is not as simple as changing a component's state: its
content is not observed, so updates will not be reflected immediately in the content is not observed, so updates will not be reflected immediately in the
user interface. There is however a mechanism to force root widgets to rerender user interface. There is however a mechanism to force root widgets to rerender
themselves whenever the environment is modified: one only needs to trigger the themselves whenever the environment is modified: one only needs to call the
`update` event on the QWeb instance. For example, a responsive environment `forceUpdate` method on the QWeb instance. For example, a responsive environment
could be programmed like this: could be done like this:
```js ```js
function setupResponsivePlugin(env) { function setupResponsivePlugin(env) {
@@ -408,7 +418,7 @@ function setupResponsivePlugin(env) {
const updateEnv = owl.utils.debounce(() => { const updateEnv = owl.utils.debounce(() => {
if (env.isMobile !== isMobile()) { if (env.isMobile !== isMobile()) {
env.isMobile = !env.isMobile; env.isMobile = !env.isMobile;
env.qweb.trigger("update"); env.qweb.forceUpdate();
} }
}, 15); }, 15);
window.addEventListener("resize", updateEnv); window.addEventListener("resize", updateEnv);
@@ -442,7 +452,8 @@ dynamic. If it is necessary to give a string, this can be done by quoting it:
`someString="'somevalue'"`. `someString="'somevalue'"`.
Note that the rendering context for the template is the component itself. This means Note that the rendering context for the template is the component itself. This means
that the template can access `state`, `props`, `env`, or any methods defined in the component. that the template can access `state` (if it exists), `props`, `env`, or any
methods defined in the component.
```xml ```xml
<div t-name="ParentComponent"> <div t-name="ParentComponent">
@@ -453,7 +464,7 @@ that the template can access `state`, `props`, `env`, or any methods defined in
```js ```js
class ParentComponent { class ParentComponent {
static components = { ChildComponent }; 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 ```js
class Form extends owl.Component { class Form extends owl.Component {
state = { text: "" }; state = useState({ text: "" });
_updateInputValue(event) { _updateInputValue(event) {
this.state.text = event.target.value; this.state.text = event.target.value;
@@ -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. or with radio buttons, or with select tags.
To help with this situation, Owl has a builtin directive `t-model`: its value To help with this situation, Owl has a builtin directive `t-model`: its value
is the (top-level) name in the state object. With the `t-model` directive, we should be an observed value in the component (usually `state.someValue`). With
can write a shorter code, equivalent to the previous example: the `t-model` directive, we can write a shorter code, equivalent to the previous
example:
```js ```js
class Form extends owl.Component { class Form extends owl.Component {
@@ -673,7 +685,7 @@ class Form extends owl.Component {
```xml ```xml
<div> <div>
<input t-model="text" /> <input t-model="state.text" />
<span t-esc="state.text" /> <span t-esc="state.text" />
</div> </div>
``` ```
@@ -683,11 +695,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
```xml ```xml
<div> <div>
<div>Text in an input: <input t-model="someVal"/></div> <div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="otherVal"/></div> <div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="someFlag"/></div> <div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection: <div>Selection:
<select t-model="color"> <select t-model="state.color">
<option value="">Select a color</option> <option value="">Select a color</option>
<option value="red">Red</option> <option value="red">Red</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
@@ -696,11 +708,11 @@ The `t-model` directive works with `<input>`, `<input type="checkbox">`,
<div> <div>
Selection with radio buttons: Selection with radio buttons:
<span> <span>
<input type="radio" name="color" id="red" value="red" t-model="color"/> <input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label> <label for="red">Red</label>
</span> </span>
<span> <span>
<input type="radio" name="color" id="blue" value="blue" t-model="color" /> <input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label> <label for="blue">Blue</label>
</span> </span>
</div> </div>
@@ -718,7 +730,7 @@ Like event handling, the `t-model` directive accepts some modifiers:
For example: For example:
```xml ```xml
<input t-model.lazy="someVal" /> <input t-model.lazy="state.someVal" />
``` ```
These modifiers can be combined. For instance, `t-model.lazy.number` will only These modifiers can be combined. For instance, `t-model.lazy.number` will only
@@ -1117,8 +1129,8 @@ For example, here is how we could implement an `ErrorBoundary` component:
``` ```
```js ```js
class ErrorBoundary extends Widget { class ErrorBoundary extends Component {
state = { error: false }; state = useState({ error: false });
catchError() { catchError() {
this.state.error = true; this.state.error = true;
+136
View File
@@ -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
View File
@@ -11,40 +11,10 @@ For example, this code will display `update` in the console:
```javascript ```javascript
const observer = new owl.Observer(); const observer = new owl.Observer();
observer.notifyCB = () => console.log("update"); observer.notifyCB = () => console.log("update");
observer.observe(obj); const obj = observer.observe( { a: { b: 1 } });
const obj = { a: { b: 1 } };
obj.a.b = 2; obj.a.b = 2;
``` ```
## Technical Limitations The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers.
Since the observer uses getters and setters, it is actually unable to react to
changes in three situations:
- adding a key to an object
- deleting a key from an object
- modifying an array by setting a new value at a given index
In those situations, we need a way to tell the observer that something happened.
This can be done by using the `set` and `delete` (only for objects) static
methods of the `Observer`.
```javascript
const observer = new owl.Observer();
const obj = { a: 1 };
observer.observe(obj);
obj.b = 2; // won't notify the change
owl.Observer.set(obj, "b", 2); // will notify the change
delete obj.b; // won't notify the change
owl.Observer.delete(obj, "b"); // will notify the change
```
```javascript
const observer = new owl.Observer();
const arr = ["a"];
observer.observe(arr);
arr[0] = "b"; // won't notify the change
owl.Observer.set(arr, 0, "b"); // will notify the change
```
+3 -1
View File
@@ -72,11 +72,13 @@ Here are a few steps that we may take to get started:
Let us now add the javascript to make it work, in `app.js`: Let us now add the javascript to make it work, in `app.js`:
```javascript ```javascript
const useState = owl.hooks.useState;
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
static template = "clickcounter"; static template = "clickcounter";
constructor() { constructor() {
super(...arguments); super(...arguments);
this.state = { value: 0 }; this.state = useState({ value: 0 });
} }
increment() { increment() {
+8
View File
@@ -9,9 +9,14 @@ build applications. Here is a complete representation of its content:
owl owl
Component Component
QWeb QWeb
useState
core core
EventBus EventBus
Observer Observer
hooks
onMounted
onWillUnmount
useState
router router
Link Link
RouteComponent RouteComponent
@@ -29,11 +34,14 @@ owl
whenReady whenReady
``` ```
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
## Reference ## Reference
- [Animations](animations.md) - [Animations](animations.md)
- [Component](component.md) - [Component](component.md)
- [Event Bus](event_bus.md) - [Event Bus](event_bus.md)
- [Hooks](hooks.md)
- [Observer](observer.md) - [Observer](observer.md)
- [QWeb](qweb.md) - [QWeb](qweb.md)
- [Router](router.md) - [Router](router.md)
+14 -26
View File
@@ -11,7 +11,8 @@ import "./props_validation";
* contains: * contains:
* *
* - the Env interface (generic type for the environment) * - the Env interface (generic type for the environment)
* - the Meta interface (the owl specific metadata attached to a component) * - the Fiber interface (owl metadata attached to a rendering)
* - the Internal interface (the owl specific metadata attached to a component)
* - the Component class * - the Component class
*/ */
@@ -46,7 +47,7 @@ export interface Fiber<Props> {
scope: any; scope: any;
vars: any; vars: any;
patchQueue: Fiber<any>[]; patchQueue: Fiber<any>[];
component: Component<any, any, any>; component: Component<any, any>;
vnode: VNode | null; vnode: VNode | null;
willPatchResult: any; willPatchResult: any;
props: Props; props: Props;
@@ -70,8 +71,8 @@ interface Internal<T extends Env, Props> {
// parent and children keys are obviously useful to setup the parent-children // parent and children keys are obviously useful to setup the parent-children
// relationship. // relationship.
parent: Component<T, any, any> | null; parent: Component<T, any> | null;
children: { [key: number]: Component<T, any, any> }; children: { [key: number]: Component<T, any> };
// children mapping: from templateID to componentID. templateID identifies a // children mapping: from templateID to componentID. templateID identifies a
// place in a template. The t-component directive needs it to be able to get // place in a template. The t-component directive needs it to be able to get
// the component instance back whenever the template is rerendered. // the component instance back whenever the template is rerendered.
@@ -91,10 +92,11 @@ interface Internal<T extends Env, Props> {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
let nextId = 1; let nextId = 1;
export class Component<T extends Env, Props extends {}, State extends {}> { export class Component<T extends Env, Props extends {}> {
readonly __owl__: Internal<Env, Props>; readonly __owl__: Internal<Env, Props>;
static template?: string | null = null; static template?: string | null = null;
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: * 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 = {}; static components = {};
env: T; env: T;
state?: State;
props: Props; props: Props;
// type of props is not easily representable in typescript... // 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; static defaultProps?: any;
refs: { 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 * hand. Other components should be created automatically by the framework (with
* the t-component directive in a template) * the t-component directive in a template)
*/ */
constructor(parent: Component<T, any, any> | T, props?: Props) { constructor(parent: Component<T, any> | T, props?: Props) {
const defaultProps = (<any>this.constructor).defaultProps; const defaultProps = (<any>this.constructor).defaultProps;
Component._current = this;
if (defaultProps) { if (defaultProps) {
props = this.__applyDefaultProps(props, defaultProps); props = this.__applyDefaultProps(props, defaultProps);
} }
@@ -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 // Pro: but creating component (by a template) is always unsafe anyway
this.props = <Props>props || <Props>{}; this.props = <Props>props || <Props>{};
let id: number = nextId++; let id: number = nextId++;
let p: Component<T, any, any> | null = null; let p: Component<T, any> | null = null;
if (parent instanceof Component) { if (parent instanceof Component) {
p = parent; p = parent;
this.env = parent.env; this.env = parent.env;
@@ -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 * Note that it does not call the __callWillUnmount method to avoid visiting
* all children many times. * all children many times.
*/ */
__destroy(parent: Component<any, any, any> | null) { __destroy(parent: Component<any, any> | null) {
const __owl__ = this.__owl__; const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted; const isMounted = __owl__.isMounted;
if (isMounted) { if (isMounted) {
@@ -586,7 +588,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
} }
} }
__owl__.render = qweb.render.bind(qweb, p._template); __owl__.render = qweb.render.bind(qweb, p._template);
this.__observeState();
return this.__render(fiber); 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). * Apply default props (only top level).
* *
@@ -693,7 +681,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/ */
__applyPatchQueue(fiber: Fiber<Props>) { __applyPatchQueue(fiber: Fiber<Props>) {
const patchQueue = fiber.patchQueue; const patchQueue = fiber.patchQueue;
let component: Component<any, any, any> = this; let component: Component<any, any> = this;
try { try {
const patchLen = patchQueue.length; const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) { for (let i = 0; i < patchLen; i++) {
@@ -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 * If there are no such component, we destroy everything. This is better than
* being in a corrupted state. * being in a corrupted state.
*/ */
function errorHandler(error: Error, component: Component<any, any, any>) { function errorHandler(error: Error, component: Component<any, any>) {
let canCatch = false; let canCatch = false;
let qweb = component.env.qweb; let qweb = component.env.qweb;
let root = component; let root = component;
+1 -1
View File
@@ -33,7 +33,7 @@ export class Observer {
} }
} }
observe(value: any, parent?: any): any { observe<T>(value: T, parent?: any): T {
if (value === null || typeof value !== "object" || value instanceof Date) { if (value === null || typeof value !== "object" || value instanceof Date) {
// fun fact: typeof null === 'object' // fun fact: typeof null === 'object'
return value; return value;
+56
View File
@@ -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);
};
}
+4
View File
@@ -11,17 +11,21 @@ import { ConnectedComponent } from "./store/connected_component";
import { Store } from "./store/store"; import { Store } from "./store/store";
import * as _utils from "./utils"; import * as _utils from "./utils";
import * as _tags from "./tags"; import * as _tags from "./tags";
import * as _hooks from "./hooks";
import { Link } from "./router/Link"; import { Link } from "./router/Link";
import { RouteComponent } from "./router/RouteComponent"; import { RouteComponent } from "./router/RouteComponent";
import { Router } from "./router/Router"; import { Router } from "./router/Router";
export { Component } from "./component/component"; export { Component } from "./component/component";
export { QWeb }; export { QWeb };
export const useState = _hooks.useState;
export const core = { EventBus, Observer }; export const core = { EventBus, Observer };
export const router = { Router, RouteComponent, Link }; export const router = { Router, RouteComponent, Link };
export const store = { Store, ConnectedComponent }; export const store = { Store, ConnectedComponent };
export const utils = _utils; export const utils = _utils;
export const tags = _tags; export const tags = _tags;
export const hooks = _hooks;
export const __info__ = {}; export const __info__ = {};
Object.defineProperty(__info__, "mode", { Object.defineProperty(__info__, "mode", {
+1 -1
View File
@@ -254,7 +254,7 @@ QWeb.addDirective({
const type = node.getAttribute("type"); const type = node.getAttribute("type");
let handler; let handler;
let event = fullName.includes(".lazy") ? "change" : "input"; let event = fullName.includes(".lazy") ? "change" : "input";
const expr = ctx.formatExpression(`state.${value}`); const expr = ctx.formatExpression(value);
if (node.tagName === "select") { if (node.tagName === "select") {
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
addNodeHook("create", `n.elm.value=${expr};`); addNodeHook("create", `n.elm.value=${expr};`);
+1 -1
View File
@@ -4,7 +4,7 @@ import { Destination, RouterEnv } from "./Router";
type Props = Destination; 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` static template = xml`
<a t-att-class="{'router-link-active': isActive }" <a t-att-class="{'router-link-active': isActive }"
t-att-href="href" t-att-href="href"
+1 -1
View File
@@ -1,7 +1,7 @@
import { Component } from "../component/component"; import { Component } from "../component/component";
import { xml } from "../tags"; import { xml } from "../tags";
export class RouteComponent extends Component<any, {}, {}> { export class RouteComponent extends Component<any, {}> {
static template = xml` static template = xml`
<t> <t>
<t <t
+1 -1
View File
@@ -7,7 +7,7 @@ import { VNode } from "../vdom/index";
type HashFunction = (a: any, b: any) => number; type HashFunction = (a: any, b: any) => number;
export class ConnectedComponent<T extends Env, P, S> extends Component<T, P, S> { export class ConnectedComponent<T extends Env, P> extends Component<T, P> {
deep: boolean = true; deep: boolean = true;
getStore(env) { getStore(env) {
return env.store; return env.store;
+10 -13
View File
@@ -1,5 +1,6 @@
import { Component, Env } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index"; import { QWeb } from "../src/qweb/index";
import { useState } from "../src/hooks";
import { import {
makeDeferred, makeDeferred,
makeTestFixture, makeTestFixture,
@@ -36,7 +37,7 @@ afterEach(() => {
fixture.remove(); fixture.remove();
}); });
class Widget extends Component<any, any, any> {} class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tests // Tests
@@ -106,7 +107,7 @@ describe("animations", () => {
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>` `<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
); );
class TestWidget extends Widget { class TestWidget extends Widget {
state = { hide: false }; state = useState({ hide: false });
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -148,7 +149,7 @@ describe("animations", () => {
`<div><span t-ref="span" t-transition="chimay">blue</span></div>` `<div><span t-ref="span" t-transition="chimay">blue</span></div>`
); );
class TestWidget extends Widget { class TestWidget extends Widget {
state = { hide: false }; state = useState({ hide: false });
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -217,7 +218,7 @@ describe("animations", () => {
class Child extends Widget {} class Child extends Widget {}
class Parent extends Widget { class Parent extends Widget {
static components = { Child: Child }; static components = { Child: Child };
state = { display: true }; state = useState({ display: true });
} }
const widget = new Parent(env); 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>` `<div><span t-if="state.flag" t-mounted="f" t-transition="chimay">blue</span></div>`
); );
class TestWidget extends Widget { class TestWidget extends Widget {
state = { flag: false }; state = useState({ flag: false });
f() {} f() {}
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -297,10 +298,8 @@ describe("animations", () => {
</templates>` </templates>`
); );
class Parent extends Widget { class Parent extends Widget {
constructor(parent) { state = useState({ flag: false });
super(parent);
this.state = { flag: false };
}
toggle() { toggle() {
this.state.flag = !this.state.flag; this.state.flag = !this.state.flag;
} }
@@ -357,10 +356,8 @@ describe("animations", () => {
class Child extends Widget {} class Child extends Widget {}
class Parent extends Widget { class Parent extends Widget {
static components = { Child }; static components = { Child };
constructor(parent) { state = useState({ flag: false });
super(parent);
this.state = { flag: false };
}
toggle() { toggle() {
this.state.flag = !this.state.flag; this.state.flag = !this.state.flag;
} }
@@ -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`] = ` exports[`t-model directive on a select 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
+147 -126
View File
@@ -1,6 +1,7 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { useState } from "../../src/hooks";
import { EventBus } from "../../src/core/event_bus"; import { EventBus } from "../../src/core/event_bus";
import { import {
makeDeferred, makeDeferred,
@@ -40,7 +41,7 @@ afterEach(() => {
fixture.remove(); fixture.remove();
}); });
class Widget extends Component<any, any, any> {} class Widget extends Component<any, any> {}
function children(w: Widget): Widget[] { function children(w: Widget): Widget[] {
const childrenMap = w.__owl__.children; const childrenMap = w.__owl__.children;
@@ -49,9 +50,9 @@ function children(w: Widget): Widget[] {
// Test components // Test components
class Counter extends Widget { class Counter extends Widget {
state = { state = useState({
counter: 0 counter: 0
}; });
inc() { inc() {
this.state.counter++; this.state.counter++;
@@ -69,10 +70,9 @@ class WidgetA extends Widget {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
describe("basic widget properties", () => { describe("basic widget properties", () => {
test("props and state are properly defined", async () => { test("props is properly defined", async () => {
const widget = new Widget(env); const widget = new Widget(env);
expect(widget.props).toEqual({}); expect(widget.props).toEqual({});
expect(widget.state).toEqual(undefined);
}); });
test("has no el after creation", async () => { test("has no el after creation", async () => {
@@ -88,7 +88,7 @@ describe("basic widget properties", () => {
test("crashes if it cannot find a template", async () => { test("crashes if it cannot find a template", async () => {
expect.assertions(1); expect.assertions(1);
class SomeWidget extends Component<any, any, any> {} class SomeWidget extends Component<any, any> {}
const widget = new SomeWidget(env); const widget = new SomeWidget(env);
try { try {
await widget.mount(fixture); await widget.mount(fixture);
@@ -133,7 +133,7 @@ describe("basic widget properties", () => {
test("changing state before first render does not trigger a render", async () => { test("changing state before first render does not trigger a render", async () => {
let renderCalls = 0; let renderCalls = 0;
class TestW extends Widget { class TestW extends Widget {
state = { drinks: 1 }; state = useState({ drinks: 1 });
async willStart() { async willStart() {
this.state.drinks++; this.state.drinks++;
} }
@@ -165,11 +165,11 @@ describe("basic widget properties", () => {
test("reconciliation alg is not confused in some specific situation", async () => { 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 // in this test, we set t-key to 4 because it was in conflict with the
// template id corresponding to the first child. // 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>`; static template = xml`<span>child</span>`;
} }
class Parent extends Component<any, any, any> { class Parent extends Component<any, any> {
static template = xml` static template = xml`
<div> <div>
<Child /> <Child />
@@ -288,7 +288,7 @@ describe("lifecycle hooks", () => {
class ParentWidget extends Widget { class ParentWidget extends Widget {
static template = xml`<div><t t-if="state.flag"><t t-component="child"/></t></div>`; static template = xml`<div><t t-if="state.flag"><t t-component="child"/></t></div>`;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
state = { flag: false }; state = useState({ flag: false });
mounted() { mounted() {
steps.push("parent: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>`); env.qweb.addTemplate("ParentWidget", `<div><t t-component="child" n="state.n"/></div>`);
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { child: ChildWidget }; static components = { child: ChildWidget };
state = { n: 1 }; state = useState({ n: 1 });
willPatch() { willPatch() {
steps.push("parent:willPatch"); steps.push("parent:willPatch");
} }
@@ -391,7 +391,7 @@ describe("lifecycle hooks", () => {
} }
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
state = { ok: false }; state = useState({ ok: false });
static components = { child: ChildWidget }; static components = { child: ChildWidget };
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
@@ -452,7 +452,7 @@ describe("lifecycle hooks", () => {
</div> </div>
`; `;
static components = { ChildWidget }; static components = { ChildWidget };
state = { ok: true }; state = useState({ ok: true });
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
@@ -471,9 +471,6 @@ describe("lifecycle hooks", () => {
willUnmount() { willUnmount() {
childUnmounted = true; childUnmounted = true;
} }
increment() {
this.state += 1;
}
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
@@ -485,7 +482,7 @@ describe("lifecycle hooks", () => {
</div> </div>
`; `;
static components = { ChildWidget }; static components = { ChildWidget };
state = { n: 0, flag: true }; state = useState({ n: 0, flag: true });
increment() { increment() {
this.state.n += 1; this.state.n += 1;
} }
@@ -567,7 +564,7 @@ describe("lifecycle hooks", () => {
} }
} }
class Parent extends Widget { class Parent extends Widget {
state = { n: 1 }; state = useState({ n: 1 });
static components = { Child: HookWidget }; static components = { Child: HookWidget };
} }
env.qweb.addTemplate("HookWidget", '<span><t t-esc="props.n"/></span>'); env.qweb.addTemplate("HookWidget", '<span><t t-esc="props.n"/></span>');
@@ -586,7 +583,7 @@ describe("lifecycle hooks", () => {
let n = 0; let n = 0;
class TestWidget extends Widget { class TestWidget extends Widget {
state = { a: 1 }; state = useState({ a: 1 });
patched() { patched() {
n++; n++;
@@ -615,7 +612,7 @@ describe("lifecycle hooks", () => {
} }
class Parent extends Widget { class Parent extends Widget {
static template = xml`<div><Child a="state.a"/></div>`; static template = xml`<div><Child a="state.a"/></div>`;
state = { a: 1 }; state = useState({ a: 1 });
static components = { Child: TestWidget }; static components = { Child: TestWidget };
} }
@@ -632,7 +629,7 @@ describe("lifecycle hooks", () => {
let n = 0; let n = 0;
class TestWidget extends Widget { class TestWidget extends Widget {
state = { a: 1 }; state = useState({ a: 1 });
patched() { patched() {
n++; n++;
@@ -657,7 +654,7 @@ describe("lifecycle hooks", () => {
class Parent extends Widget { class Parent extends Widget {
static template = xml`<div><Child val="state.val"/></div>`; static template = xml`<div><Child val="state.val"/></div>`;
static components = { Child: TestWidget }; static components = { Child: TestWidget };
state = { val: 42 }; state = useState({ val: 42 });
} }
const widget = new Parent(env); const widget = new Parent(env);
@@ -694,7 +691,7 @@ describe("lifecycle hooks", () => {
</div> </div>
`; `;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
state = { flag: false }; state = useState({ flag: false });
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -725,7 +722,7 @@ describe("lifecycle hooks", () => {
</div> </div>
`; `;
static components = { child: ChildWidget }; static components = { child: ChildWidget };
state = { n: 1 }; state = useState({ n: 1 });
willPatch() { willPatch() {
steps.push("parent:willPatch"); steps.push("parent:willPatch");
return "leffe"; return "leffe";
@@ -777,7 +774,7 @@ describe("lifecycle hooks", () => {
</div> </div>
`; `;
static components = { ChildWidget }; static components = { ChildWidget };
state = { n: 1, flag: true }; state = useState({ n: 1, flag: true });
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
@@ -855,14 +852,14 @@ describe("destroy method", () => {
}); });
test("destroying a widget before being mounted", async () => { test("destroying a widget before being mounted", async () => {
class GrandChild extends Component<any, any, any> { class GrandChild extends Component<any, any> {
static template = xml` static template = xml`
<span> <span>
<t t-esc="props.val.val"/> <t t-esc="props.val.val"/>
</span> </span>
`; `;
} }
class Child extends Component<any, any, any> { class Child extends Component<any, any> {
static template = xml` static template = xml`
<span> <span>
<GrandChild t-if="state.flag" val="something"/> <GrandChild t-if="state.flag" val="something"/>
@@ -870,7 +867,7 @@ describe("destroy method", () => {
</span> </span>
`; `;
static components = { GrandChild }; static components = { GrandChild };
state = { val: 33, flag: false }; state = useState({ val: 33, flag: false });
doSomething() { doSomething() {
this.state.val = 12; this.state.val = 12;
this.state.flag = true; this.state.flag = true;
@@ -880,14 +877,14 @@ describe("destroy method", () => {
return { val: this.state.val }; return { val: this.state.val };
} }
} }
class Parent extends Component<any, any, any> { class Parent extends Component<any, any> {
static template = xml` static template = xml`
<div t-on-some-event="doStuff"> <div t-on-some-event="doStuff">
<Child /> <Child />
</div> </div>
`; `;
static components = { Child }; static components = { Child };
state = { p: 1 }; state = useState({ p: 1 });
doStuff() { doStuff() {
this.state.p = 2; this.state.p = 2;
} }
@@ -921,17 +918,17 @@ describe("composition", () => {
}); });
test("can use dynamic components (the class) if given", async () => { 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>`; 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>`; 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"/>`; static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { state = useState({
child: "a" child: "a"
}; });
get myComponent() { get myComponent() {
return this.state.child === "a" ? A : B; return this.state.child === "a" ? A : B;
} }
@@ -1010,7 +1007,7 @@ describe("composition", () => {
</div> </div>
`; `;
static components = { Widget }; static components = { Widget };
state = { list: <any>[] }; state = useState({ list: <any>[] });
willPatch() { willPatch() {
expect(this.refs.child).toBeUndefined(); expect(this.refs.child).toBeUndefined();
} }
@@ -1037,7 +1034,7 @@ describe("composition", () => {
); );
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { Widget }; static components = { Widget };
state = { child1: true, child2: false }; state = useState({ child1: true, child2: false });
count = 0; count = 0;
mounted() { mounted() {
expect(this.refs.child1).toBeDefined(); expect(this.refs.child1).toBeDefined();
@@ -1098,7 +1095,7 @@ describe("composition", () => {
</div>` </div>`
); );
class ParentWidget extends Widget { class ParentWidget extends Widget {
state = { items: [1, 2, 3] }; state = useState({ items: [1, 2, 3] });
static components = { Child: Widget }; static components = { Child: Widget };
} }
const parent = new ParentWidget(env); const parent = new ParentWidget(env);
@@ -1144,7 +1141,7 @@ describe("composition", () => {
test("sub components are destroyed if no longer in dom, then recreated", async () => { 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>`); env.qweb.addTemplate("ParentWidget", `<div><t t-if="state.ok"><Counter /></t></div>`);
class ParentWidget extends Widget { class ParentWidget extends Widget {
state = { ok: true }; state = useState({ ok: true });
static components = { Counter }; static components = { Counter };
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
@@ -1168,7 +1165,7 @@ describe("composition", () => {
`<div><t t-if="state.ok"><Counter t-keepalive="1"/></t></div>` `<div><t t-if="state.ok"><Counter t-keepalive="1"/></t></div>`
); );
class ParentWidget extends Widget { class ParentWidget extends Widget {
state = { ok: true }; state = useState({ ok: true });
static components = { Counter }; static components = { Counter };
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
@@ -1198,7 +1195,7 @@ describe("composition", () => {
); );
class InputWidget extends Widget {} class InputWidget extends Widget {}
class ParentWidget extends Widget { class ParentWidget extends Widget {
state = { ok: true }; state = useState({ ok: true });
static components = { InputWidget }; static components = { InputWidget };
} }
env.qweb.addTemplate("InputWidget", "<input/>"); env.qweb.addTemplate("InputWidget", "<input/>");
@@ -1229,7 +1226,7 @@ describe("composition", () => {
</templates>`); </templates>`);
class ChildWidget extends Widget {} class ChildWidget extends Widget {}
class ParentWidget extends Widget { class ParentWidget extends Widget {
state = { ok: true }; state = useState({ ok: true });
static components = { ChildWidget }; static components = { ChildWidget };
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
@@ -1266,9 +1263,9 @@ describe("composition", () => {
</div>` </div>`
); );
class Parent extends Widget { class Parent extends Widget {
state = { state = useState({
numbers: [1, 2, 3] numbers: [1, 2, 3]
}; });
static components = { ChildWidget }; static components = { ChildWidget };
} }
const parent = new Parent(env); const parent = new Parent(env);
@@ -1288,9 +1285,10 @@ describe("composition", () => {
let n = 1; let n = 1;
env.qweb.addTemplate("ChildWidget", `<span><t t-esc="state.n"/></span>`); env.qweb.addTemplate("ChildWidget", `<span><t t-esc="state.n"/></span>`);
class ChildWidget extends Widget { class ChildWidget extends Widget {
state: any;
constructor(parent) { constructor(parent) {
super(parent); super(parent);
this.state = { n }; this.state = useState({ n });
n++; n++;
} }
} }
@@ -1305,9 +1303,9 @@ describe("composition", () => {
); );
class Parent extends Widget { class Parent extends Widget {
static template = "parent"; static template = "parent";
state = { state = useState({
numbers: [1, 2, 3] numbers: [1, 2, 3]
}; });
static components = { ChildWidget }; static components = { ChildWidget };
} }
const parent = new Parent(env); const parent = new Parent(env);
@@ -1341,7 +1339,7 @@ describe("composition", () => {
); );
class Parent extends Widget { class Parent extends Widget {
state = { flag: false }; state = useState({ flag: false });
static components = { ChildWidget }; static components = { ChildWidget };
} }
const parent = new Parent(env); const parent = new Parent(env);
@@ -1380,7 +1378,7 @@ describe("composition", () => {
class SubWidget extends Widget {} class SubWidget extends Widget {}
class Parent extends Widget { class Parent extends Widget {
static components = { SubWidget }; 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); const parent = new Parent(env);
await parent.mount(fixture); await parent.mount(fixture);
@@ -1412,7 +1410,7 @@ describe("composition", () => {
class SubWidget extends Widget {} class SubWidget extends Widget {}
class Parent extends Widget { class Parent extends Widget {
static components = { SubWidget }; static components = { SubWidget };
state = { blips: [{ a: "a", id: 1 }] }; state = useState({ blips: [{ a: "a", id: 1 }] });
} }
const parent = new Parent(env); const parent = new Parent(env);
await parent.mount(fixture); await parent.mount(fixture);
@@ -1423,7 +1421,7 @@ describe("composition", () => {
env.qweb.addTemplate("ParentWidget", `<div><t t-component="{{state.widget}}"/></div>`); env.qweb.addTemplate("ParentWidget", `<div><t t-component="{{state.widget}}"/></div>`);
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { WidgetB }; static components = { WidgetB };
state = { widget: "WidgetB" }; state = useState({ widget: "WidgetB" });
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -1435,7 +1433,7 @@ describe("composition", () => {
env.qweb.addTemplate("ParentWidget", `<div><t t-component="Widget{{state.widget}}"/></div>`); env.qweb.addTemplate("ParentWidget", `<div><t t-component="Widget{{state.widget}}"/></div>`);
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { WidgetB }; static components = { WidgetB };
state = { widget: "B" }; state = useState({ widget: "B" });
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -1451,12 +1449,12 @@ describe("props evaluation ", () => {
state: { someval: number }; state: { someval: number };
constructor(parent: Parent, props: { value: number }) { constructor(parent: Parent, props: { value: number }) {
super(parent); super(parent);
this.state = { someval: props.value }; this.state = useState({ someval: props.value });
} }
} }
class Parent extends Widget { class Parent extends Widget {
static components = { child: Child }; static components = { child: Child };
state = { val: 42 }; state = useState({ val: 42 });
} }
env.qweb.addTemplate("Child", `<span><t t-esc="state.someval"/></span>`); 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 Child extends Widget {}
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { child: Child }; static components = { child: Child };
state = { a: true, b: false }; state = useState({ a: true, b: false });
} }
env.qweb.addTemplate("Child", `<div class="c"/>`); env.qweb.addTemplate("Child", `<div class="c"/>`);
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
@@ -1579,11 +1577,11 @@ describe("class and style attributes with t-component", () => {
</templates>`); </templates>`);
class Child extends Widget { class Child extends Widget {
state = { d: true }; state = useState({ d: true });
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { Child }; static components = { Child };
state = { b: true }; state = useState({ b: true });
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -1620,11 +1618,11 @@ describe("class and style attributes with t-component", () => {
</templates>`); </templates>`);
class Child extends Widget { class Child extends Widget {
state = { d: true }; state = useState({ d: true });
} }
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { Child }; static components = { Child };
state = { b: true }; state = useState({ b: true });
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -1658,7 +1656,7 @@ describe("class and style attributes with t-component", () => {
</templates>`); </templates>`);
class App extends Widget { class App extends Widget {
state = { c: true }; state = useState({ c: true });
mounted() { mounted() {
this.el!.classList.add("user"); this.el!.classList.add("user");
} }
@@ -1701,7 +1699,7 @@ describe("class and style attributes with t-component", () => {
); );
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { child: Widget }; static components = { child: Widget };
state = { style: "font-size: 20px" }; state = useState({ style: "font-size: 20px" });
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -1977,7 +1975,7 @@ describe("other directives with t-component", () => {
class Child extends Widget {} class Child extends Widget {}
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { child: Child }; static components = { child: Child };
state = { flag: true }; state = useState({ flag: true });
} }
env.qweb.addTemplate("Child", "<span>hey</span>"); env.qweb.addTemplate("Child", "<span>hey</span>");
@@ -2007,7 +2005,7 @@ describe("other directives with t-component", () => {
class Child extends Widget {} class Child extends Widget {}
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { child: Child }; static components = { child: Child };
state = { flag: true }; state = useState({ flag: true });
} }
env.qweb.addTemplate("Child", "<span>hey</span>"); env.qweb.addTemplate("Child", "<span>hey</span>");
@@ -2033,7 +2031,7 @@ describe("other directives with t-component", () => {
class Child extends Widget {} class Child extends Widget {}
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { child: Child }; static components = { child: Child };
state = { flag: true }; state = useState({ flag: true });
} }
env.qweb.addTemplate("Child", "<span>hey</span>"); env.qweb.addTemplate("Child", "<span>hey</span>");
@@ -2059,7 +2057,7 @@ describe("other directives with t-component", () => {
class Child extends Widget {} class Child extends Widget {}
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { child: Child }; static components = { child: Child };
state = { flag: true }; state = useState({ flag: true });
} }
env.qweb.addTemplate("Child", "<span>hey</span>"); env.qweb.addTemplate("Child", "<span>hey</span>");
@@ -2127,7 +2125,7 @@ describe("random stuff/miscellaneous", () => {
class Child extends Widget {} class Child extends Widget {}
class Parent extends Widget { class Parent extends Widget {
static components = { child: Child }; 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>`); 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 Child extends Widget {}
class Parent extends Widget { class Parent extends Widget {
static components = { child: Child }; 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>`); 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 { class C extends TestWidget {
static components = { D, E, F }; static components = { D, E, F };
name = "C"; name = "C";
state = { flag: true }; state = useState({ flag: true });
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
@@ -2315,7 +2313,7 @@ describe("random stuff/miscellaneous", () => {
const SUBTEMPLATE = xml`<span><t t-esc="state.n"/></span>`; const SUBTEMPLATE = xml`<span><t t-esc="state.n"/></span>`;
class Parent extends Widget { class Parent extends Widget {
static template = xml`<div><t t-call="${SUBTEMPLATE}"/></div>`; static template = xml`<div><t t-call="${SUBTEMPLATE}"/></div>`;
state = { n: 42 }; state = useState({ n: 42 });
} }
const widget = new Parent(env); const widget = new Parent(env);
@@ -2360,7 +2358,7 @@ describe("async rendering", () => {
} }
class W extends Widget { class W extends Widget {
static components = { Child }; static components = { Child };
state = { val: 1 }; state = useState({ val: 1 });
} }
const w = new W(env); const w = new W(env);
@@ -2406,7 +2404,7 @@ describe("async rendering", () => {
); );
class Parent extends Widget { class Parent extends Widget {
static components = { ChildA, ChildB }; static components = { ChildA, ChildB };
state = { flagA: false, flagB: false }; state = useState({ flagA: false, flagB: false });
} }
const parent = new Parent(env); const parent = new Parent(env);
await parent.mount(fixture); await parent.mount(fixture);
@@ -2453,7 +2451,7 @@ describe("async rendering", () => {
); );
class Parent extends Widget { class Parent extends Widget {
static components = { ChildA, ChildB }; static components = { ChildA, ChildB };
state = { valA: 1, valB: 2, flagB: false }; state = useState({ valA: 1, valB: 2, flagB: false });
} }
const parent = new Parent(env); const parent = new Parent(env);
await parent.mount(fixture); await parent.mount(fixture);
@@ -2538,7 +2536,7 @@ describe("async rendering", () => {
); );
class Parent extends Widget { class Parent extends Widget {
static components = { Child }; static components = { Child };
state = { flag: true, val: "Framboise Lindemans" }; state = useState({ flag: true, val: "Framboise Lindemans" });
} }
const parent = new Parent(env); const parent = new Parent(env);
await parent.mount(fixture); await parent.mount(fixture);
@@ -2588,7 +2586,7 @@ describe("async rendering", () => {
} }
class Parent extends Widget { class Parent extends Widget {
static components = { ChildA, ChildB }; static components = { ChildA, ChildB };
state = { valA: 1, valB: 2, flag: false }; state = useState({ valA: 1, valB: 2, flag: false });
} }
const parent = new Parent(env); const parent = new Parent(env);
await parent.mount(fixture); await parent.mount(fixture);
@@ -2627,7 +2625,7 @@ describe("async rendering", () => {
} }
class Parent extends Widget { class Parent extends Widget {
static components = { Child, AsyncChild }; static components = { Child, AsyncChild };
state = { val: 0 }; state = useState({ val: 0 });
updateApp() { updateApp() {
this.state.val++; this.state.val++;
@@ -2676,7 +2674,7 @@ describe("async rendering", () => {
} }
class Parent extends Widget { class Parent extends Widget {
static components = { Child, AsyncChild }; static components = { Child, AsyncChild };
state = { val: 0 }; state = useState({ val: 0 });
updateApp() { updateApp() {
this.state.val++; this.state.val++;
@@ -2720,7 +2718,7 @@ describe("async rendering", () => {
let def; let def;
class Child extends Widget { class Child extends Widget {
state = { val: 0 }; state = useState({ val: 0 });
increment() { increment() {
this.state.val++; this.state.val++;
@@ -2733,7 +2731,7 @@ describe("async rendering", () => {
} }
class Parent extends Widget { class Parent extends Widget {
static components = { Child, AsyncChild }; static components = { Child, AsyncChild };
state = { val: 0 }; state = useState({ val: 0 });
updateApp() { updateApp() {
this.state.val++; this.state.val++;
@@ -2909,7 +2907,7 @@ describe("widget and observable state", () => {
test("widget is rerendered when its state is changed", async () => { test("widget is rerendered when its state is changed", async () => {
env.qweb.addTemplate("TestWidget", `<div><t t-esc="state.drink"/></div>`); env.qweb.addTemplate("TestWidget", `<div><t t-esc="state.drink"/></div>`);
class TestWidget extends Widget { class TestWidget extends Widget {
state = { drink: "water" }; state = useState({ drink: "water" });
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -2934,7 +2932,7 @@ describe("widget and observable state", () => {
} }
} }
class Parent extends Widget { class Parent extends Widget {
state = { obj: { coffee: 1 } }; state = useState({ obj: { coffee: 1 } });
static components = { Child }; static components = { Child };
} }
const parent = new Parent(env); const parent = new Parent(env);
@@ -2986,7 +2984,7 @@ describe("t-mounted directive", () => {
test("combined with a t-if", async () => { test("combined with a t-if", async () => {
env.qweb.addTemplate("TestWidget", `<div><input t-if="state.flag" t-mounted="f"/></div>`); env.qweb.addTemplate("TestWidget", `<div><input t-if="state.flag" t-mounted="f"/></div>`);
class TestWidget extends Widget { class TestWidget extends Widget {
state = { flag: false }; state = useState({ flag: false });
f() {} f() {}
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -3103,7 +3101,7 @@ describe("t-slot directive", () => {
class Dialog extends Widget {} class Dialog extends Widget {}
class Parent extends Widget { class Parent extends Widget {
static components = { Dialog }; static components = { Dialog };
state = { val: 0 }; state = useState({ val: 0 });
doSomething() { doSomething() {
this.state.val++; this.state.val++;
} }
@@ -3140,7 +3138,7 @@ describe("t-slot directive", () => {
class Link extends Widget {} class Link extends Widget {}
class App 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 }; static components = { Link };
} }
@@ -3179,7 +3177,7 @@ describe("t-slot directive", () => {
class Link extends Widget {} class Link extends Widget {}
class App 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 }; static components = { Link };
} }
@@ -3216,7 +3214,7 @@ describe("t-slot directive", () => {
class Link extends Widget {} class Link extends Widget {}
class App extends Widget { class App extends Widget {
state = { user: { id: 1, name: "Aaron" } }; state = useState({ user: { id: 1, name: "Aaron" } });
static components = { Link }; static components = { Link };
} }
@@ -3249,7 +3247,7 @@ describe("t-slot directive", () => {
class Dialog extends Widget {} class Dialog extends Widget {}
class Parent extends Widget { class Parent extends Widget {
static components = { Dialog }; static components = { Dialog };
state = { val: 0 }; state = useState({ val: 0 });
doSomething() { doSomething() {
this.state.val++; this.state.val++;
} }
@@ -3462,7 +3460,7 @@ describe("t-slot directive", () => {
class GenericComponent extends Widget {} class GenericComponent extends Widget {}
class App extends Widget { class App extends Widget {
static components = { GenericComponent, SomeComponent }; static components = { GenericComponent, SomeComponent };
state = { val: 4 }; state = useState({ val: 4 });
inc() { inc() {
this.state.val++; this.state.val++;
@@ -3478,14 +3476,14 @@ describe("t-slot directive", () => {
}); });
test("slots and wrapper components", async () => { test("slots and wrapper components", async () => {
class Link extends Component<any, any, any> { class Link extends Component<any, any> {
static template = xml` static template = xml`
<a href="abc"> <a href="abc">
<t t-slot="default"/> <t t-slot="default"/>
</a>`; </a>`;
} }
class A extends Component<any, any, any> { class A extends Component<any, any> {
static template = xml`<Link>hey</Link>`; static template = xml`<Link>hey</Link>`;
static components = { Link: Link }; static components = { Link: Link };
} }
@@ -3502,12 +3500,12 @@ describe("t-model directive", () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<input t-model="text"/> <input t-model="state.text"/>
<span><t t-esc="state.text"/></span> <span><t t-esc="state.text"/></span>
</div> </div>
</templates>`); </templates>`);
class SomeComponent extends Widget { class SomeComponent extends Widget {
state = { text: "" }; state = useState({ text: "" });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3521,11 +3519,34 @@ describe("t-model directive", () => {
expect(env.qweb.templates.SomeComponent.fn.toString()).toMatchSnapshot(); 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 () => { test("on an input, type=checkbox", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<input type="checkbox" t-model="flag"/> <input type="checkbox" t-model="state.flag"/>
<span> <span>
<t t-if="state.flag">yes</t> <t t-if="state.flag">yes</t>
<t t-else="1">no</t> <t t-else="1">no</t>
@@ -3533,7 +3554,7 @@ describe("t-model directive", () => {
</div> </div>
</templates>`); </templates>`);
class SomeComponent extends Widget { class SomeComponent extends Widget {
state = { flag: false }; state = useState({ flag: false });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3556,12 +3577,12 @@ describe("t-model directive", () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<textarea t-model="text"/> <textarea t-model="state.text"/>
<span><t t-esc="state.text"/></span> <span><t t-esc="state.text"/></span>
</div> </div>
</templates>`); </templates>`);
class SomeComponent extends Widget { class SomeComponent extends Widget {
state = { text: "" }; state = useState({ text: "" });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3578,13 +3599,13 @@ describe("t-model directive", () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<input type="radio" id="one" value="One" t-model="choice"/> <input type="radio" id="one" value="One" t-model="state.choice"/>
<input type="radio" id="two" value="Two" t-model="choice"/> <input type="radio" id="two" value="Two" t-model="state.choice"/>
<span>Choice: <t t-esc="state.choice"/></span> <span>Choice: <t t-esc="state.choice"/></span>
</div> </div>
</templates>`); </templates>`);
class SomeComponent extends Widget { class SomeComponent extends Widget {
state = { choice: "" }; state = useState({ choice: "" });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3615,7 +3636,7 @@ describe("t-model directive", () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<select t-model="color"> <select t-model="state.color">
<option value="">Please select one</option> <option value="">Please select one</option>
<option value="red">Red</option> <option value="red">Red</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
@@ -3624,7 +3645,7 @@ describe("t-model directive", () => {
</div> </div>
</templates>`); </templates>`);
class SomeComponent extends Widget { class SomeComponent extends Widget {
state = { color: "" }; state = useState({ color: "" });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3650,14 +3671,14 @@ describe("t-model directive", () => {
class SomeComponent extends Widget { class SomeComponent extends Widget {
static template = xml` static template = xml`
<div> <div>
<select t-model="color"> <select t-model="state.color">
<option value="">Please select one</option> <option value="">Please select one</option>
<option value="red">Red</option> <option value="red">Red</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
</select> </select>
</div> </div>
`; `;
state = { color: "red" }; state = useState({ color: "red" });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3669,11 +3690,11 @@ describe("t-model directive", () => {
class SomeComponent extends Widget { class SomeComponent extends Widget {
static template = xml` static template = xml`
<div> <div>
<input t-model="something.text"/> <input t-model="state.something.text"/>
<span><t t-esc="state.something.text"/></span> <span><t t-esc="state.something.text"/></span>
</div> </div>
`; `;
state = { something: { text: "" } }; state = useState({ something: { text: "" } });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3691,11 +3712,11 @@ describe("t-model directive", () => {
class SomeComponent extends Widget { class SomeComponent extends Widget {
static template = xml` static template = xml`
<div> <div>
<input t-model.lazy="text"/> <input t-model.lazy="state.text"/>
<span><t t-esc="state.text"/></span> <span><t t-esc="state.text"/></span>
</div> </div>
`; `;
state = { text: "" }; state = useState({ text: "" });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3719,11 +3740,11 @@ describe("t-model directive", () => {
class SomeComponent extends Widget { class SomeComponent extends Widget {
static template = xml` static template = xml`
<div t-name="SomeComponent"> <div t-name="SomeComponent">
<input t-model.trim="text"/> <input t-model.trim="state.text"/>
<span><t t-esc="state.text"/></span> <span><t t-esc="state.text"/></span>
</div> </div>
`; `;
state = { text: "" }; state = useState({ text: "" });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3738,11 +3759,11 @@ describe("t-model directive", () => {
class SomeComponent extends Widget { class SomeComponent extends Widget {
static template = xml` static template = xml`
<div> <div>
<input t-model.number="number"/> <input t-model.number="state.number"/>
<span><t t-esc="state.number"/></span> <span><t t-esc="state.number"/></span>
</div> </div>
`; `;
state = { number: 0 }; state = useState({ number: 0 });
} }
const comp = new SomeComponent(env); const comp = new SomeComponent(env);
await comp.mount(fixture); await comp.mount(fixture);
@@ -3823,14 +3844,14 @@ describe("component error handling (catchError)", () => {
env.qweb.on("error", null, handler); env.qweb.on("error", null, handler);
class ErrorComponent extends Widget {} class ErrorComponent extends Widget {}
class ErrorBoundary extends Widget { class ErrorBoundary extends Widget {
state = { error: false }; state = useState({ error: false });
catchError() { catchError() {
this.state.error = true; this.state.error = true;
} }
} }
class App extends Widget { class App extends Widget {
state = { flag: false }; state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent }; static components = { ErrorBoundary, ErrorComponent };
} }
const app = new App(env); const app = new App(env);
@@ -3862,7 +3883,7 @@ describe("component error handling (catchError)", () => {
</templates>`); </templates>`);
class ErrorComponent extends Widget {} class ErrorComponent extends Widget {}
class App extends Widget { class App extends Widget {
state = { flag: false }; state = useState({ flag: false });
static components = { ErrorComponent }; static components = { ErrorComponent };
} }
const app = new App(env); const app = new App(env);
@@ -3899,7 +3920,7 @@ describe("component error handling (catchError)", () => {
</templates>`); </templates>`);
class ErrorComponent extends Widget {} class ErrorComponent extends Widget {}
class ErrorBoundary extends Widget { class ErrorBoundary extends Widget {
state = { error: false }; state = useState({ error: false });
catchError() { catchError() {
this.state.error = true; this.state.error = true;
@@ -3943,7 +3964,7 @@ describe("component error handling (catchError)", () => {
} }
} }
class ErrorBoundary extends Widget { class ErrorBoundary extends Widget {
state = { error: false }; state = useState({ error: false });
catchError() { catchError() {
this.state.error = true; this.state.error = true;
@@ -3986,7 +4007,7 @@ describe("component error handling (catchError)", () => {
} }
} }
class ErrorBoundary extends Widget { class ErrorBoundary extends Widget {
state = { error: false }; state = useState({ error: false });
catchError() { catchError() {
this.state.error = true; this.state.error = true;
@@ -4025,7 +4046,7 @@ describe("component error handling (catchError)", () => {
} }
} }
class ErrorBoundary extends Widget { class ErrorBoundary extends Widget {
state = { error: false }; state = useState({ error: false });
catchError() { catchError() {
this.state.error = true; this.state.error = true;
@@ -4060,14 +4081,14 @@ describe("component error handling (catchError)", () => {
} }
} }
class ErrorBoundary extends Widget { class ErrorBoundary extends Widget {
state = { error: false }; state = useState({ error: false });
catchError() { catchError() {
this.state.error = true; this.state.error = true;
} }
} }
class App extends Widget { class App extends Widget {
state = { message: "abc" }; state = useState({ message: "abc" });
static components = { ErrorBoundary, ErrorComponent }; static components = { ErrorBoundary, ErrorComponent };
} }
const app = new App(env); 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> <span t-name="Child"><button t-on-click="inc">click</button>child<t t-esc="state.val"/></span>
</templates>`); </templates>`);
class Child extends Widget { class Child extends Widget {
state = { val: 1 }; state = useState({ val: 1 });
inc() { inc() {
this.state.val++; this.state.val++;
} }
@@ -4169,7 +4190,7 @@ describe("top level sub widgets", () => {
<t t-if="!state.flag"><OtherChild /></t> <t t-if="!state.flag"><OtherChild /></t>
</t> </t>
`; `;
state = { flag: true }; state = useState({ flag: true });
static components = { Child, OtherChild }; static components = { Child, OtherChild };
} }
let parent = new Parent(env); let parent = new Parent(env);
@@ -4240,7 +4261,7 @@ describe("unmounting and remounting", () => {
static template = xml` static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span> <span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`; `;
state = { n: 2 }; state = useState({ n: 2 });
__render(f) { __render(f) {
steps.push("render"); steps.push("render");
return super.__render(f); return super.__render(f);
@@ -4264,7 +4285,7 @@ describe("unmounting and remounting", () => {
</div> </div>
`; `;
static components = { Child }; static components = { Child };
state = { val: 1, flag: true }; state = useState({ val: 1, flag: true });
} }
const widget = new Parent(env); const widget = new Parent(env);
@@ -4282,7 +4303,7 @@ describe("unmounting and remounting", () => {
static template = xml` static template = xml`
<div><t t-esc="state.val"/></div> <div><t t-esc="state.val"/></div>
`; `;
state = { val: 1 }; state = useState({ val: 1 });
willUnmount() { willUnmount() {
this.state.val = 3; this.state.val = 3;
} }
@@ -4342,7 +4363,7 @@ describe("dynamic root nodes", () => {
<t t-if="!state.flag"><div>abc</div></t> <t t-if="!state.flag"><div>abc</div></t>
</t> </t>
`; `;
state = { flag: true }; state = useState({ flag: true });
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -4370,7 +4391,7 @@ describe("dynamic root nodes", () => {
</t> </t>
`; `;
static components = { ChildA, ChildB }; static components = { ChildA, ChildB };
state = { flag: true }; state = useState({ flag: true });
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
+1 -1
View File
@@ -22,7 +22,7 @@ afterEach(() => {
QWeb.dev = dev; QWeb.dev = dev;
}); });
class Widget extends Component<any, any, any> {} class Widget extends Component<any, any> {}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Tests // Tests
+132
View File
@@ -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"
]);
});
});
+1 -1
View File
@@ -1333,5 +1333,5 @@ describe("properly support svg", () => {
expect(renderToString(qweb, "test")).toBe( expect(renderToString(qweb, "test")).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>` `<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
); );
}); });
}); });
+2 -2
View File
@@ -30,7 +30,7 @@ describe("Link component", () => {
</div> </div>
</templates> </templates>
`); `);
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { Link: Link }; static components = { Link: Link };
} }
@@ -61,7 +61,7 @@ describe("Link component", () => {
</div> </div>
</templates> </templates>
`); `);
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { Link: Link }; static components = { Link: Link };
} }
+7 -7
View File
@@ -32,9 +32,9 @@ describe("RouteComponent", () => {
<span t-name="Users">Users</span> <span t-name="Users">Users</span>
</templates> </templates>
`); `);
class About extends Component<any, any, any> {} class About extends Component<any, any> {}
class Users extends Component<any, any, any> {} class Users extends Component<any, any> {}
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { RouteComponent }; static components = { RouteComponent };
} }
@@ -64,8 +64,8 @@ describe("RouteComponent", () => {
<span t-name="Book">Book <t t-esc="props.title"/></span> <span t-name="Book">Book <t t-esc="props.title"/></span>
</templates> </templates>
`); `);
class Book extends Component<any, any, any> {} class Book extends Component<any, any> {}
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { RouteComponent }; static components = { RouteComponent };
} }
@@ -86,12 +86,12 @@ describe("RouteComponent", () => {
<span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span> <span t-name="Book">Book <t t-esc="props.title"/>|<t t-esc="incVal"/></span>
</templates> </templates>
`); `);
class Book extends Component<any, any, any> { class Book extends Component<any, any> {
get incVal() { get incVal() {
return this.props.val + 1; return this.props.val + 1;
} }
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { RouteComponent }; static components = { RouteComponent };
} }
+47 -46
View File
@@ -1,6 +1,7 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { ConnectedComponent } from "../../src/store/connected_component"; import { ConnectedComponent } from "../../src/store/connected_component";
import { Store } from "../../src/store/store"; import { Store } from "../../src/store/store";
import { useState } from "../../src/hooks";
import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers"; import { makeTestEnv, makeTestFixture, nextTick, makeDeferred } from "../helpers";
describe("connecting a component to store", () => { describe("connecting a component to store", () => {
@@ -27,8 +28,8 @@ describe("connecting a component to store", () => {
<span t-name="Todo"><t t-esc="props.msg"/></span> <span t-name="Todo"><t t-esc="props.msg"/></span>
</templates> </templates>
`); `);
class Todo extends Component<any, any, any> {} class Todo extends Component<any, any> {}
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static components = { Todo }; static components = { Todo };
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
@@ -70,7 +71,7 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
} }
@@ -111,7 +112,7 @@ describe("connecting a component to store", () => {
<span t-name="Todo"><t t-esc="props.msg"/></span> <span t-name="Todo"><t t-esc="props.msg"/></span>
</templates> </templates>
`); `);
class Todo extends Component<any, any, any> {} class Todo extends Component<any, any> {}
const store = new Store({ const store = new Store({
state: { todos: [] }, state: { todos: [] },
@@ -121,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 components = { Todo };
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { todos: s.todos }; return { todos: s.todos };
@@ -160,7 +161,7 @@ describe("connecting a component to store", () => {
}); });
(<any>env).store = store; (<any>env).store = store;
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { value: s.value }; return { value: s.value };
} }
@@ -187,7 +188,7 @@ describe("connecting a component to store", () => {
<div t-name="Child"/> <div t-name="Child"/>
</templates> </templates>
`); `);
class Child extends ConnectedComponent<any, any, any> { class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return s; return s;
} }
@@ -199,12 +200,12 @@ describe("connecting a component to store", () => {
} }
} }
class Parent extends Component<any, any, any> { class Parent extends Component<any, any> {
static components = { Child }; static components = { Child };
state: any;
constructor(env: Env) { constructor(env: Env) {
super(env); super(env);
this.state = { child: true }; this.state = useState({ child: true });
} }
} }
@@ -240,14 +241,14 @@ describe("connecting a component to store", () => {
</div> </div>
</templates> </templates>
`); `);
class TodoItem extends ConnectedComponent<any, any, any> { class TodoItem extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) { static mapStoreToProps(state, props) {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return todo; return todo;
} }
} }
class TodoList extends ConnectedComponent<any, any, any> { class TodoList extends ConnectedComponent<any, any> {
static components = { TodoItem }; static components = { TodoItem };
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { todos: state.todos }; return { todos: state.todos };
@@ -294,7 +295,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class TodoItem extends ConnectedComponent<any, any, any> { class TodoItem extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props, getters) { static mapStoreToProps(state, props, getters) {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return { return {
@@ -304,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 components = { TodoItem };
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { todos: state.todos }; return { todos: state.todos };
@@ -330,15 +331,15 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Beer extends ConnectedComponent<any, any, any> { class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) { static mapStoreToProps(state, props) {
return state.beers[props.id]; return state.beers[props.id];
} }
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { Beer }; static components = { Beer };
state = { beerId: 1 }; state = useState({ beerId: 1 });
} }
const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } }; const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } };
@@ -363,7 +364,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 }; return { beers: state.beers, otherKey: 1 };
} }
@@ -403,7 +404,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Beer extends ConnectedComponent<any, any, any> { class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(state, props) { static mapStoreToProps(state, props) {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
@@ -413,9 +414,9 @@ describe("connecting a component to store", () => {
} }
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { Beer }; static components = { Beer };
state = { beerId: 0 }; state = useState({ beerId: 0 });
} }
const actions = { const actions = {
@@ -470,7 +471,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Beer extends ConnectedComponent<any, any, any> { class Beer extends ConnectedComponent<any, any> {
static mapStoreToProps(storeState, props) { static mapStoreToProps(storeState, props) {
return { return {
selected: storeState.beers[props.id], selected: storeState.beers[props.id],
@@ -480,9 +481,9 @@ describe("connecting a component to store", () => {
} }
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { Beer }; static components = { Beer };
state = { beerId: 0 }; state = useState({ beerId: 0 });
} }
const actions = { const actions = {
@@ -559,13 +560,13 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Child extends ConnectedComponent<any, any, any> { class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s, props) { static mapStoreToProps(s, props) {
steps.push("child"); steps.push("child");
return { msg: s.msg[props.key] }; return { msg: s.msg[props.key] };
} }
} }
class Parent extends ConnectedComponent<any, any, any> { class Parent extends ConnectedComponent<any, any> {
static components = { Child }; static components = { Child };
static mapStoreToProps(s) { static mapStoreToProps(s) {
steps.push("parent"); steps.push("parent");
@@ -608,13 +609,13 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class Child extends ConnectedComponent<any, any, any> { class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s, props) { static mapStoreToProps(s, props) {
steps.push("child"); steps.push("child");
return { msg: s.messages[props.someId] }; return { msg: s.messages[props.someId] };
} }
} }
class Parent extends ConnectedComponent<any, any, any> { class Parent extends ConnectedComponent<any, any> {
static components = { Child }; static components = { Child };
static mapStoreToProps(s) { static mapStoreToProps(s) {
steps.push("parent"); steps.push("parent");
@@ -686,7 +687,7 @@ describe("connecting a component to store", () => {
let renderCount = 0; let renderCount = 0;
let fCount = 0; let fCount = 0;
class TodoItem extends ConnectedComponent<any, any, any> { class TodoItem extends ConnectedComponent<any, any> {
state = { isEditing: false }; state = { isEditing: false };
static mapStoreToProps(state, ownProps) { static mapStoreToProps(state, ownProps) {
fCount++; fCount++;
@@ -703,7 +704,7 @@ describe("connecting a component to store", () => {
return super.__render(f); return super.__render(f);
} }
} }
class TodoApp extends ConnectedComponent<any, any, any> { class TodoApp extends ConnectedComponent<any, any> {
static components = { TodoItem }; static components = { TodoItem };
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { return {
@@ -764,7 +765,7 @@ describe("connecting a component to store", () => {
let renderCount = 0; let renderCount = 0;
let fCount = 0; let fCount = 0;
class TodoItem extends ConnectedComponent<any, any, any> { class TodoItem extends ConnectedComponent<any, any> {
state = { isEditing: false }; state = { isEditing: false };
static mapStoreToProps(state, ownProps) { static mapStoreToProps(state, ownProps) {
@@ -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 components = { TodoItem };
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { return {
@@ -817,7 +818,7 @@ describe("connecting a component to store", () => {
</templates> </templates>
`); `);
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return { msg: s.msg }; return { msg: s.msg };
} }
@@ -871,11 +872,11 @@ describe("connected components and default values", () => {
</templates> </templates>
`); `);
class Greeter extends ConnectedComponent<any, any, any> { class Greeter extends ConnectedComponent<any, any> {
static defaultProps = { recipient: "John" }; static defaultProps = { recipient: "John" };
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { Greeter }; static components = { Greeter };
} }
@@ -895,11 +896,11 @@ describe("connected components and default values", () => {
</templates> </templates>
`); `);
class Greeter extends ConnectedComponent<any, any, any> { class Greeter extends ConnectedComponent<any, any> {
static defaultProps = { recipient: "John" }; static defaultProps = { recipient: "John" };
} }
class App extends Component<any, any, any> { class App extends Component<any, any> {
static components = { Greeter }; static components = { Greeter };
} }
@@ -936,7 +937,7 @@ describe("connected components and default values", () => {
</templates> </templates>
`); `);
class Message extends ConnectedComponent<any, any, any> { class Message extends ConnectedComponent<any, any> {
static defaultProps = { showId: true }; static defaultProps = { showId: true };
static mapStoreToProps = function(state, ownProps) { static mapStoreToProps = function(state, ownProps) {
return { return {
@@ -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 components = { Message };
static defaultProps = { showMessages: true }; static defaultProps = { showMessages: true };
static mapStoreToProps = function(state, ownProps) { 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 components = { Thread };
static defaultProps = { threadId: 1 }; static defaultProps = { threadId: 1 };
} }
@@ -1018,15 +1019,15 @@ describe("connected components and default values", () => {
<div t-name="Child"><t t-esc="storeProps.val"/></div> <div t-name="Child"><t t-esc="storeProps.val"/></div>
</templates> </templates>
`); `);
class Child extends ConnectedComponent<any, any, any> { class Child extends ConnectedComponent<any, any> {
static mapStoreToProps(s) { static mapStoreToProps(s) {
return s; return s;
} }
} }
class Parent extends Component<any, any, any> { class Parent extends Component<any, any> {
static components = { Child }; static components = { Child };
state = { child: true }; state = useState({ child: true });
} }
class TestStore extends Store { class TestStore extends Store {
@@ -1062,7 +1063,7 @@ describe("connected components and default values", () => {
</templates> </templates>
`); `);
class App extends ConnectedComponent<any, any, any> { class App extends ConnectedComponent<any, any> {
static mapStoreToProps = function(state) { static mapStoreToProps = function(state) {
return { return {
counter: state.counter counter: state.counter
@@ -1143,14 +1144,14 @@ describe("various scenarios", () => {
</div> </div>
</templates> </templates>
`); `);
class Attachment extends ConnectedComponent<any, any, any> { class Attachment extends ConnectedComponent<any, any> {
static mapStoreToProps(state, ownProps) { static mapStoreToProps(state, ownProps) {
return { return {
name: state.attachments[ownProps.id].name name: state.attachments[ownProps.id].name
}; };
} }
} }
class Message extends ConnectedComponent<any, any, any> { class Message extends ConnectedComponent<any, any> {
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { return {
attachmentIds: state.messages[10].attachmentIds attachmentIds: state.messages[10].attachmentIds
+5 -7
View File
@@ -206,17 +206,15 @@ describe("snabbdom", function() {
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
// verify that svg tag automatically gets svg namespace // verify that svg tag automatically gets svg namespace
const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]); const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]);
// need to add namespace manually. it is usually done by the template // need to add namespace manually. it is usually done by the template
// compiler // compiler
addNS(vnode.data,(vnode as any).children, vnode.sel); addNS(vnode.data, (vnode as any).children, vnode.sel);
elm = patch(vnode0, vnode) elm = patch(vnode0, vnode).elm;
.elm;
expect(elm.namespaceURI).toBe(SVGNamespace); expect(elm.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); expect(elm.firstChild.namespaceURI).toBe(SVGNamespace);
expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace); expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace);
}); });
test("receives classes in selector", function() { test("receives classes in selector", function() {
+3 -2
View File
@@ -1,10 +1,11 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js"; import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const useState = owl.hooks.useState;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Likes Counter Widget // Likes Counter Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class Counter extends owl.Component { class Counter extends owl.Component {
state = { counter: 0 }; state = useState({ counter: 0 });
increment() { increment() {
this.state.counter++; this.state.counter++;
@@ -32,7 +33,7 @@ class Message extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class App extends owl.Component { class App extends owl.Component {
static components = { Message }; static components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false }; state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
mounted() { mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`); this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
+2 -2
View File
@@ -12,11 +12,11 @@
</div> </div>
<div class="flags"> <div class="flags">
<div> <div>
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/> <input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label> <label for="multipleflag">Do it 20x</label>
</div> </div>
<div> <div>
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" /> <input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label> <label for="clearFlag">Clear after</label>
</div> </div>
</div> </div>
+5 -5
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
const useState = owl.hooks.useState;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -156,9 +156,9 @@ Promise.all([loadTemplates(), owl.utils.whenReady()]).then(start);
class TabbedEditor extends owl.Component { class TabbedEditor extends owl.Component {
constructor(parent, props) { constructor(parent, props) {
super(parent, props); super(parent, props);
this.state = { this.state = useState({
currentTab: props.js ? "js" : props.xml ? "xml" : "css" currentTab: props.js ? "js" : props.xml ? "xml" : "css"
}; });
this.setTab = owl.utils.debounce(this.setTab, 250, true); this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {}; this.sessions = {};
@@ -250,7 +250,7 @@ class App extends owl.Component {
this.version = owl.__info__.version; this.version = owl.__info__.version;
this.SAMPLES = SAMPLES; this.SAMPLES = SAMPLES;
this.state = { this.state = useState({
js: SAMPLES[0].code, js: SAMPLES[0].code,
css: SAMPLES[0].css || "", css: SAMPLES[0].css || "",
xml: SAMPLES[0].xml || DEFAULT_XML, xml: SAMPLES[0].xml || DEFAULT_XML,
@@ -259,7 +259,7 @@ class App extends owl.Component {
splitLayout: true, splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2), leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null topPanelHeight: null
}; });
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true); this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true); this.runCode = owl.utils.debounce(this.runCode, 250, true);
+111 -41
View File
@@ -1,7 +1,8 @@
const COMPONENTS = `// In this example, we show how components can be defined and created. const COMPONENTS = `// In this example, we show how components can be defined and created.
const { Component, useState } = owl;
class Greeter extends owl.Component { class Greeter extends Component {
state = { word: 'Hello' }; state = useState({ word: 'Hello' });
toggle() { toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi' this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'
@@ -9,9 +10,9 @@ class Greeter extends owl.Component {
} }
// Main root component // Main root component
class App extends owl.Component { class App extends Component {
static components = { Greeter }; static components = { Greeter };
state = { name: 'World'}; state = useState({ name: 'World'});
} }
// Application setup // 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 const ANIMATION = `// The goal of this component is to see how the t-transition directive can be
// used to generate simple transition effects. // used to generate simple transition effects.
const { Component, useState } = owl;
class Counter extends owl.Component { class Counter extends Component {
state = { value: 0 }; state = useState({ value: 0 });
increment() { increment() {
this.state.value++; this.state.value++;
} }
} }
class App extends owl.Component { class App extends Component {
state = { flag: false, componentFlag: false, numbers: [] }; state = useState({ flag: false, componentFlag: false, numbers: [] });
static components = { Counter }; static components = { Counter };
toggle(key) { 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 // methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what // clicking on the two main buttons, and look into the console to see what
// happens. // happens.
const { Component, useState } = owl;
class DemoComponent extends owl.Component { class DemoComponent extends Component {
constructor() { constructor() {
super(...arguments); super(...arguments);
this.state = { n: 0 }; this.state = useState({ n: 0 });
console.log("constructor"); console.log("constructor");
} }
async willStart() { async willStart() {
@@ -213,9 +216,9 @@ class DemoComponent extends owl.Component {
} }
} }
class App extends owl.Component { class App extends Component {
static components = { DemoComponent }; static components = { DemoComponent };
state = { n: 0, flag: true }; state = useState({ n: 0, flag: true });
increment() { increment() {
this.state.n++; this.state.n++;
@@ -259,12 +262,69 @@ const LIFECYCLE_CSS = `button {
width: 250px; width: 250px;
}`; }`;
const HOOKS_DEMO = `// In this example, we show how hooks can be used or defined.
const {useState, onMounted, onWillUnmount} = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onWillUnmount(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class App extends owl.Component {
// simple state hook (reactive object)
counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
increment() {
this.counter.value++;
}
}
// Application setup
const qweb = new owl.QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
`;
const HOOKS_DEMO_XML = `<templates>
<div t-name="App">
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
`;
const HOOKS_CSS = `button {
width: 120px;
height: 35px;
font-size: 16px;
}`;
const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the const TODO_APP_STORE = `// This example is an implementation of the TodoList application, from the
// www.todomvc.com project. This is a non trivial application with some // www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence. // interesting user interactions. It uses the local storage for persistence.
// //
// In this implementation, we use the owl Store class to manage the state. It // In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store. // is very similar to the VueX store.
const { Component, useState } = owl;
const ENTER_KEY = 13; const ENTER_KEY = 13;
const ESC_KEY = 27; const ESC_KEY = 27;
@@ -335,8 +395,8 @@ function makeStore() {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// TodoItem // TodoItem
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoItem extends owl.Component { class TodoItem extends Component {
state = { isEditing: false }; state = useState({ isEditing: false });
removeTodo() { removeTodo() {
this.env.store.dispatch("removeTodo", this.props.id); this.env.store.dispatch("removeTodo", this.props.id);
@@ -389,7 +449,7 @@ class TodoItem extends owl.Component {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoApp extends owl.store.ConnectedComponent { class TodoApp extends owl.store.ConnectedComponent {
static components = { TodoItem }; static components = { TodoItem };
state = { filter: "all" }; state = useState({ filter: "all" });
static mapStoreToProps(state) { static mapStoreToProps(state) {
return { 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 // Note that the t-on-click event, defined in the App template, is executed in
// the context of the App component, even though it is inside the Card component // the context of the App component, even though it is inside the Card component
const { Component, useState } = owl;
class Card extends owl.Component { class Card extends Component {
state = { showContent: true }; state = useState({ showContent: true });
toggleDisplay() { toggleDisplay() {
this.state.showContent = !this.state.showContent; this.state.showContent = !this.state.showContent;
} }
} }
class Counter extends owl.Component { class Counter extends Component {
state = {val: 1}; state = useState({val: 1});
inc() { inc() {
this.state.val++; this.state.val++;
@@ -1070,9 +1131,9 @@ class Counter extends owl.Component {
} }
// Main root component // Main root component
class App extends owl.Component { class App extends Component {
static components = {Card, Counter}; static components = {Card, Counter};
state = {a: 1, b: 3}; state = useState({a: 1, b: 3});
inc(key, delta) { inc(key, delta) {
this.state[key] += delta; this.state[key] += delta;
@@ -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 // However, we don't want renderings of the other sub component to be delayed
// because of the slow component. We use the 't-asyncroot' directive for this // because of the slow component. We use the 't-asyncroot' directive for this
// purpose. Try removing it to see the difference. // purpose. Try removing it to see the difference.
const { Component, useState } = owl;
class SlowComponent extends owl.Component { class SlowComponent extends Component {
willUpdateProps() { willUpdateProps() {
// simulate a component that needs to perform async stuff (e.g. an RPC) // simulate a component that needs to perform async stuff (e.g. an RPC)
// with the updated props before re-rendering itself // 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}; static components = {SlowComponent, NotificationList};
state = { value: 0, notifs: [] }; state = useState({ value: 0, notifs: [] });
increment() { increment() {
this.state.value++; 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. // data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally // Note that there are two controls with t-model="color": they are totally
// synchronized. // synchronized.
const { Component, useState } = owl;
class Form extends owl.Component { class Form extends Component {
state = { state = useState({
text: "", text: "",
othertext: "", othertext: "",
number: 11, number: 11,
color: "", color: "",
bool: false bool: false
}; });
} }
// Application setup // Application setup
@@ -1271,20 +1334,20 @@ const FORM_XML = `<templates>
<div t-name="Form"> <div t-name="Form">
<h1>Form</h1> <h1>Form</h1>
<div> <div>
Text (immediate): <input t-model="text"/> Text (immediate): <input t-model="state.text"/>
</div> </div>
<div> <div>
Other text (lazy): <input t-model.lazy="othertext"/> Other text (lazy): <input t-model.lazy="state.othertext"/>
</div> </div>
<div> <div>
Number: <input t-model.number="number"/> Number: <input t-model.number="state.number"/>
</div> </div>
<div> <div>
Boolean: <input type="checkbox" t-model="bool"/> Boolean: <input type="checkbox" t-model="state.bool"/>
</div> </div>
<div> <div>
Color, with a select: Color, with a select:
<select t-model="color"> <select t-model="state.color">
<option value="">Select a color</option> <option value="">Select a color</option>
<option value="red">Red</option> <option value="red">Red</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
@@ -1292,8 +1355,8 @@ const FORM_XML = `<templates>
</div> </div>
<div> <div>
Color, with radio buttons: Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="color"/><label for="red">Red</label></span> <span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="color"/><label for="blue">Blue</label></span> <span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
</div> </div>
<hr/> <hr/>
<h1>State</h1> <h1>State</h1>
@@ -1316,18 +1379,19 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - minimal width/height // - minimal width/height
// - better heuristic for initial window position // - better heuristic for initial window position
// - ... // - ...
const { Component, useState } = owl;
class HelloWorld extends owl.Component {} class HelloWorld extends Component {}
class Counter extends owl.Component { class Counter extends Component {
state = { value: 0 }; state = useState({ value: 0 });
inc() { inc() {
this.state.value++; this.state.value++;
} }
} }
class Window extends owl.Component { class Window extends Component {
get style() { get style() {
let { width, height, top, left, zindex } = this.props.info; let { width, height, top, left, zindex } = this.props.info;
@@ -1366,7 +1430,7 @@ class Window extends owl.Component {
} }
} }
class WindowManager extends owl.Component { class WindowManager extends Component {
static components = { Window }; static components = { Window };
windows = []; windows = [];
nextId = 1; nextId = 1;
@@ -1406,7 +1470,7 @@ class WindowManager extends owl.Component {
} }
} }
class App extends owl.Component { class App extends Component {
static components = { WindowManager }; static components = { WindowManager };
addWindow(name) { addWindow(name) {
@@ -1569,6 +1633,12 @@ export const SAMPLES = [
xml: LIFECYCLE_DEMO_XML, xml: LIFECYCLE_DEMO_XML,
css: LIFECYCLE_CSS css: LIFECYCLE_CSS
}, },
{
description: "Hooks",
code: HOOKS_DEMO,
xml: HOOKS_DEMO_XML,
css: HOOKS_CSS
},
{ {
description: "Todo List App (with store)", description: "Todo List App (with store)",
code: TODO_APP_STORE, code: TODO_APP_STORE,