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