diff --git a/README.md b/README.md
index 3ac72531..a4aa76da 100644
--- a/README.md
+++ b/README.md
@@ -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! []
`;
- 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.
diff --git a/doc/component.md b/doc/component.md
index 09f55cec..3e248d88 100644
--- a/doc/component.md
+++ b/doc/component.md
@@ -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
@@ -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
-
+
```
@@ -683,11 +695,11 @@ The `t-model` directive works with ``, ``,
```xml
");
+ component.unmount();
+ expect(fixture.innerHTML).toBe("");
+ expect(steps).toEqual([
+ "hook:mounted1",
+ "hook:mounted2",
+ "hook:willunmount2",
+ "hook:willunmount1"
+ ]);
+ });
+});
diff --git a/tests/qweb/qweb.test.ts b/tests/qweb/qweb.test.ts
index 61043e1f..16e1b3a1 100644
--- a/tests/qweb/qweb.test.ts
+++ b/tests/qweb/qweb.test.ts
@@ -1333,5 +1333,5 @@ describe("properly support svg", () => {
expect(renderToString(qweb, "test")).toBe(
``
);
- });
+});
});
diff --git a/tests/router/Link.test.ts b/tests/router/Link.test.ts
index 3148c255..e777b0cd 100644
--- a/tests/router/Link.test.ts
+++ b/tests/router/Link.test.ts
@@ -30,7 +30,7 @@ describe("Link component", () => {
`);
- class App extends Component {
+ class App extends Component {
static components = { Link: Link };
}
@@ -61,7 +61,7 @@ describe("Link component", () => {
`);
- class App extends Component {
+ class App extends Component {
static components = { Link: Link };
}
diff --git a/tests/router/RouteComponent.test.ts b/tests/router/RouteComponent.test.ts
index ddbeda89..3ae57f9e 100644
--- a/tests/router/RouteComponent.test.ts
+++ b/tests/router/RouteComponent.test.ts
@@ -32,9 +32,9 @@ describe("RouteComponent", () => {
Users
`);
- class About extends Component {}
- class Users extends Component {}
- class App extends Component {
+ class About extends Component {}
+ class Users extends Component {}
+ class App extends Component {
static components = { RouteComponent };
}
@@ -64,8 +64,8 @@ describe("RouteComponent", () => {
Book
`);
- class Book extends Component {}
- class App extends Component {
+ class Book extends Component {}
+ class App extends Component {
static components = { RouteComponent };
}
@@ -86,12 +86,12 @@ describe("RouteComponent", () => {
Book |
`);
- class Book extends Component {
+ class Book extends Component {
get incVal() {
return this.props.val + 1;
}
}
- class App extends Component {
+ class App extends Component {
static components = { RouteComponent };
}
diff --git a/tests/store/connected_component.test.ts b/tests/store/connected_component.test.ts
index 78754e83..9a65bc3c 100644
--- a/tests/store/connected_component.test.ts
+++ b/tests/store/connected_component.test.ts
@@ -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", () => {
`);
- class Todo extends Component {}
- class App extends ConnectedComponent {
+ class Todo extends Component {}
+ class App extends ConnectedComponent {
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 {
+ class App extends ConnectedComponent {
static mapStoreToProps(s) {
return { todos: s.todos };
}
@@ -111,7 +112,7 @@ describe("connecting a component to store", () => {
`);
- class Todo extends Component {}
+ class Todo extends Component {}
const store = new Store({
state: { todos: [] },
@@ -121,7 +122,7 @@ describe("connecting a component to store", () => {
}
}
});
- class App extends ConnectedComponent {
+ class App extends ConnectedComponent {
static components = { Todo };
static mapStoreToProps(s) {
return { todos: s.todos };
@@ -160,7 +161,7 @@ describe("connecting a component to store", () => {
});
(env).store = store;
- class App extends ConnectedComponent {
+ class App extends ConnectedComponent {
static mapStoreToProps(s) {
return { value: s.value };
}
@@ -187,7 +188,7 @@ describe("connecting a component to store", () => {
`);
- class Child extends ConnectedComponent {
+ class Child extends ConnectedComponent {
static mapStoreToProps(s) {
return s;
}
@@ -199,12 +200,12 @@ describe("connecting a component to store", () => {
}
}
- class Parent extends Component {
+ class Parent extends Component {
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", () => {
`);
- class TodoItem extends ConnectedComponent {
+ class TodoItem extends ConnectedComponent {
static mapStoreToProps(state, props) {
const todo = state.todos.find(t => t.id === props.id);
return todo;
}
}
- class TodoList extends ConnectedComponent {
+ class TodoList extends ConnectedComponent {
static components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
@@ -294,7 +295,7 @@ describe("connecting a component to store", () => {
`);
- class TodoItem extends ConnectedComponent {
+ class TodoItem extends ConnectedComponent {
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 {
+ class TodoList extends ConnectedComponent {
static components = { TodoItem };
static mapStoreToProps(state) {
return { todos: state.todos };
@@ -330,15 +331,15 @@ describe("connecting a component to store", () => {
`);
- class Beer extends ConnectedComponent {
+ class Beer extends ConnectedComponent {
static mapStoreToProps(state, props) {
return state.beers[props.id];
}
}
- class App extends Component {
+ class App extends Component {
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", () => {
`);
- class App extends ConnectedComponent {
+ class App extends ConnectedComponent {
static mapStoreToProps(state) {
return { beers: state.beers, otherKey: 1 };
}
@@ -403,7 +404,7 @@ describe("connecting a component to store", () => {
`);
- class Beer extends ConnectedComponent {
+ class Beer extends ConnectedComponent {
static mapStoreToProps(state, props) {
return {
selected: state.beers[props.id],
@@ -413,9 +414,9 @@ describe("connecting a component to store", () => {
}
}
- class App extends Component {
+ class App extends Component {
static components = { Beer };
- state = { beerId: 0 };
+ state = useState({ beerId: 0 });
}
const actions = {
@@ -470,7 +471,7 @@ describe("connecting a component to store", () => {
`);
- class Beer extends ConnectedComponent {
+ class Beer extends ConnectedComponent {
static mapStoreToProps(storeState, props) {
return {
selected: storeState.beers[props.id],
@@ -480,9 +481,9 @@ describe("connecting a component to store", () => {
}
}
- class App extends Component {
+ class App extends Component {
static components = { Beer };
- state = { beerId: 0 };
+ state = useState({ beerId: 0 });
}
const actions = {
@@ -559,13 +560,13 @@ describe("connecting a component to store", () => {
`);
- class Child extends ConnectedComponent {
+ class Child extends ConnectedComponent {
static mapStoreToProps(s, props) {
steps.push("child");
return { msg: s.msg[props.key] };
}
}
- class Parent extends ConnectedComponent {
+ class Parent extends ConnectedComponent {
static components = { Child };
static mapStoreToProps(s) {
steps.push("parent");
@@ -608,13 +609,13 @@ describe("connecting a component to store", () => {
`);
- class Child extends ConnectedComponent {
+ class Child extends ConnectedComponent {
static mapStoreToProps(s, props) {
steps.push("child");
return { msg: s.messages[props.someId] };
}
}
- class Parent extends ConnectedComponent {
+ class Parent extends ConnectedComponent {
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 {
+ class TodoItem extends ConnectedComponent {
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 {
+ class TodoApp extends ConnectedComponent {
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 {
+ class TodoItem extends ConnectedComponent {
state = { isEditing: false };
static mapStoreToProps(state, ownProps) {
@@ -782,7 +783,7 @@ describe("connecting a component to store", () => {
}
}
- class TodoApp extends ConnectedComponent {
+ class TodoApp extends ConnectedComponent {
static components = { TodoItem };
static mapStoreToProps(state) {
return {
@@ -817,7 +818,7 @@ describe("connecting a component to store", () => {
`);
- class App extends ConnectedComponent {
+ class App extends ConnectedComponent {
static mapStoreToProps(s) {
return { msg: s.msg };
}
@@ -871,11 +872,11 @@ describe("connected components and default values", () => {
`);
- class Greeter extends ConnectedComponent {
+ class Greeter extends ConnectedComponent {
static defaultProps = { recipient: "John" };
}
- class App extends Component {
+ class App extends Component {
static components = { Greeter };
}
@@ -895,11 +896,11 @@ describe("connected components and default values", () => {
`);
- class Greeter extends ConnectedComponent {
+ class Greeter extends ConnectedComponent {
static defaultProps = { recipient: "John" };
}
- class App extends Component {
+ class App extends Component {
static components = { Greeter };
}
@@ -936,7 +937,7 @@ describe("connected components and default values", () => {
`);
- class Message extends ConnectedComponent {
+ class Message extends ConnectedComponent {
static defaultProps = { showId: true };
static mapStoreToProps = function(state, ownProps) {
return {
@@ -945,7 +946,7 @@ describe("connected components and default values", () => {
};
}
- class Thread extends ConnectedComponent {
+ class Thread extends ConnectedComponent {
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 {
+ class App extends Component {
static components = { Thread };
static defaultProps = { threadId: 1 };
}
@@ -1018,15 +1019,15 @@ describe("connected components and default values", () => {
`);
- class Child extends ConnectedComponent {
+ class Child extends ConnectedComponent {
static mapStoreToProps(s) {
return s;
}
}
- class Parent extends Component {
+ class Parent extends Component {
static components = { Child };
- state = { child: true };
+ state = useState({ child: true });
}
class TestStore extends Store {
@@ -1062,7 +1063,7 @@ describe("connected components and default values", () => {
`);
- class App extends ConnectedComponent {
+ class App extends ConnectedComponent {
static mapStoreToProps = function(state) {
return {
counter: state.counter
@@ -1143,14 +1144,14 @@ describe("various scenarios", () => {
`);
- class Attachment extends ConnectedComponent {
+ class Attachment extends ConnectedComponent {
static mapStoreToProps(state, ownProps) {
return {
name: state.attachments[ownProps.id].name
};
}
}
- class Message extends ConnectedComponent {
+ class Message extends ConnectedComponent {
static mapStoreToProps(state) {
return {
attachmentIds: state.messages[10].attachmentIds
diff --git a/tests/vdom.test.ts b/tests/vdom.test.ts
index c9fb15e8..73e90cb7 100644
--- a/tests/vdom.test.ts
+++ b/tests/vdom.test.ts
@@ -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() {
diff --git a/tools/benchmarks/owl-master/app.js b/tools/benchmarks/owl-master/app.js
index 1e096dfe..18020a68 100644
--- a/tools/benchmarks/owl-master/app.js
+++ b/tools/benchmarks/owl-master/app.js
@@ -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})`);
diff --git a/tools/benchmarks/owl-master/templates.xml b/tools/benchmarks/owl-master/templates.xml
index d246b616..c8724c0a 100644
--- a/tools/benchmarks/owl-master/templates.xml
+++ b/tools/benchmarks/owl-master/templates.xml
@@ -12,11 +12,11 @@
-
+
-
+
diff --git a/tools/playground/app.js b/tools/playground/app.js
index 974141c8..be42db16 100644
--- a/tools/playground/app.js
+++ b/tools/playground/app.js
@@ -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);
diff --git a/tools/playground/samples.js b/tools/playground/samples.js
index 6314608c..ab48ca1a 100644
--- a/tools/playground/samples.js
+++ b/tools/playground/samples.js
@@ -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 = `
+
+
+
Mouse: ,
+
+
+`;
+
+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 = `
Form
- Text (immediate):
+ Text (immediate):
- Other text (lazy):
+ Other text (lazy):
- Number:
+ Number:
- Boolean:
+ Boolean:
Color, with a select:
-
+
@@ -1292,8 +1355,8 @@ const FORM_XML = `