[IMP] hooks: implement useRef hook

closes #194
This commit is contained in:
Géry Debongnie
2019-09-26 21:56:01 +02:00
parent 91b9e78182
commit 5cdaa7b473
20 changed files with 321 additions and 171 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ const app = new App({ qweb: new QWeb() });
app.mount(document.body); app.mount(document.body);
``` ```
Note that the counter component is made reactive with the `useState` hook. Note that the counter component is made reactive with the [`useState` hook](doc/hooks.md#usestate).
More interesting examples can be found on the More interesting examples can be found on the
[playground](https://odoo.github.io/owl/playground) application. [playground](https://odoo.github.io/owl/playground) application.
+49 -38
View File
@@ -47,7 +47,6 @@ exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in Q
Rendering a component generates a virtual dom representation Rendering a component generates a virtual dom representation
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way. of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
## Example ## Example
Let us have a look at a simple component: Let us have a look at a simple component:
@@ -79,7 +78,6 @@ Owl will use the component's name as template name. Here,
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 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. [observed](observer.md), and any change to it will cause a rerendering.
## Reference ## Reference
An Owl component is a small class which represent a component or some UI element. An Owl component is a small class which represent a component or some UI element.
@@ -122,9 +120,6 @@ constructor.
in some case. Note that `props` are owned by the parent, not by the component. in some case. Note that `props` are owned by the parent, not by the component.
As such, it should not ever be modified by the component!! As such, it should not ever be modified by the component!!
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
or sub components defined by a `t-ref` directive in the component's template.
### Static Properties ### Static Properties
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to - **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
@@ -517,24 +512,24 @@ class ParentComponent {
``` ```
There is an even more dynamic way to use `t-component`: its value can be an There is an even more dynamic way to use `t-component`: its value can be an
expression evaluating to an actual component class. In that case, this is the expression evaluating to an actual component class. In that case, this is the
class that will be used to create the component: class that will be used to create the component:
```js ```js
class A extends Component<any, any, any> { class A extends Component<any, any, any> {
static template = xml`<span>child a</span>`; static template = xml`<span>child a</span>`;
} }
class B extends Component<any, any, any> { class B extends Component<any, any, any> {
static template = xml`<span>child b</span>`; static template = xml`<span>child b</span>`;
} }
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
static template = xml`<t t-component="myComponent" t-key="state.child"/>`; static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
state = { child: "a" }; state = { child: "a" };
get myComponent() { get myComponent() {
return this.state.child === "a" ? A : B; return this.state.child === "a" ? A : B;
} }
} }
``` ```
@@ -967,44 +962,60 @@ Examples:
### References ### References
The `t-ref` directive helps a component keep reference to some inside part of it. The `useRef` hook is useful when we need a way to interact with some inside part
Like the `t-on` directive, it can work either on a DOM node, or on a component: of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
more detail.
As a short example, here is how we could set the focus on a given input:
```xml ```xml
<div> <div>
<div t-ref="someDiv"/> <input t-ref="input"/>
<SubComponent t-ref="someComponent"/> <button t-on-click="focusInput">Click</button>
</div> </div>
``` ```
In this example, the component will be able to access the `div` and the component
inside the special `refs` variable:
```js ```js
this.refs.someDiv; import { useRef } from "owl/hooks";
this.refs.someComponent;
class SomeComponent extends Component {
inputRef = useRef("input");
focusInput() {
this.inputRef.el.focus();
}
}
``` ```
This is useful for various usecases: for example, integrating with an external The `useRef` hook can also be used to get a reference to an instance of a sub
library that needs to render itself inside an actual DOM node. Or for calling component rendered by Owl. In that case, we need to access it with the `comp`
some method on a sub component. property instead of `el`:
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-component` directives). For example, if we have
`id` set to 44 in the rendering context,
```xml ```xml
<div t-ref="component_{{id}}"/> <div>
<SubComponent t-ref="sub"/>
<button t-on-click="doSomething">Click</button>
</div>
``` ```
```js ```js
this.refs.component_44; import { useRef } from "owl/hooks";
class SomeComponent extends Component {
static components = { SubComponent };
subRef = useRef("sub");
doSomething() {
this.subRef.comp.doSomeThingElse();
}
}
``` ```
Note that these two examples uses the suffix `ref` to name the reference. This
is not mandatory, but it is a useful convention, so we do not forget to access
it with the `el` or `comp` suffix.
### Slots ### Slots
To make generic components, it is useful to be able for a parent component to _inject_ To make generic components, it is useful to be able for a parent component to _inject_
@@ -1163,19 +1174,19 @@ env.qweb.on("error", null, function(error) {
### Functional Components ### Functional Components
Owl does not exactly have functional components. However, there is an extremely Owl does not exactly have functional components. However, there is an extremely
close alternative: calling sub templates. close alternative: calling sub templates.
A stateless functional component in react is usually some kind of function that A stateless functional component in react is usually some kind of function that
maps props to a virtual dom (often with `jsx`). So, basically, almost like a maps props to a virtual dom (often with `jsx`). So, basically, almost like a
template rendered with `props`. In Owl, this can be done by template rendered with `props`. In Owl, this can be done by
simply defining a template, that will access the `props` object: simply defining a template, that will access the `props` object:
```js ```js
const Welcome = xml`<h1>Hello, {props.name}</h1>`; const Welcome = xml`<h1>Hello, {props.name}</h1>`;
class MyComponent extends Component { class MyComponent extends Component {
static template = xml` static template = xml`
<div> <div>
<t t-call=${Welcome}/> <t t-call=${Welcome}/>
<div>something</div> <div>something</div>
+86 -33
View File
@@ -5,14 +5,16 @@
- [Overview](#overview) - [Overview](#overview)
- [Example](#example) - [Example](#example)
- [Reference](#reference) - [Reference](#reference)
- [One Rule](#one-rule) - [One Rule](#one-rule)
- [`useState`](#usestate) - [`useState`](#usestate)
- [`onMounted`](#onmounted) - [`onMounted`](#onmounted)
- [`onWillUnmount`](#onWillUnmount) - [`onWillUnmount`](#onwillunmount)
- [`useRef`](#useref)
## Overview ## Overview
Hooks were popularised by React as a way to solve the following issues: Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components - help reusing stateful logic between components
- help organizing code by feature in complex components - help organizing code by feature in complex components
- use state in functional components, without writing a class. - use state in functional components, without writing a class.
@@ -22,51 +24,49 @@ Owl hooks serve the same purpose, except that they work for class components
there seems to be the misconception that hooks are in opposition to class. This there seems to be the misconception that hooks are in opposition to class. This
is clearly not true, as shown by Owl hooks). is clearly not true, as shown by Owl hooks).
Hooks works beautifully with Owl components: they solve the problems mentioned Hooks works beautifully with Owl components: they solve the problems mentioned
above, and in particular, they are the perfect way to make your component above, and in particular, they are the perfect way to make your component
reactive. reactive.
## Example ## Example
Here is the classical example of a non trivial hook to track the mouse position. Here is the classical example of a non trivial hook to track the mouse position.
```js ```js
const {useState, onMounted, onWillUnmount} = owl.hooks; const { useState, onMounted, onWillUnmount } = owl.hooks;
// We define here a custom behaviour: this hook tracks the state of the mouse // We define here a custom behaviour: this hook tracks the state of the mouse
// position // position
function useMouse() { function useMouse() {
const position = useState({x:0, y: 0}); const position = useState({ x: 0, y: 0 });
function update(e) { function update(e) {
position.x = e.clientX; position.x = e.clientX;
position.y = e.clientY; position.y = e.clientY;
} }
onMounted(() => { onMounted(() => {
window.addEventListener('mousemove', update); window.addEventListener("mousemove", update);
}); });
onWillUnmount(() => { onWillUnmount(() => {
window.removeEventListener('mousemove', update); window.removeEventListener("mousemove", update);
}); });
return position; return position;
} }
// Main root component // Main root component
class App extends owl.Component { class App extends owl.Component {
static template = xml` static template = xml`
<div t-name="App"> <div t-name="App">
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div> <div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>`; </div>`;
// this hooks is bound to the 'mouse' property. // this hooks is bound to the 'mouse' property.
mouse = useMouse(); mouse = useMouse();
} }
``` ```
Note that we use the prefix `use` for hooks, just like in React. This is just Note that we use the prefix `use` for hooks, just like in React. This is just
a convention. a convention.
## Reference ## Reference
@@ -79,22 +79,22 @@ constructor (or in class fields):
```js ```js
// ok // ok
class SomeComponent extends Component { class SomeComponent extends Component {
state = useState(); state = useState({value: 0});
} }
// also ok // also ok
class SomeComponent extends Component { class SomeComponent extends Component {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.state = useState(); this.state = useState({value: 0});
} }
} }
// not ok: this is executed after the constructor is called // not ok: this is executed after the constructor is called
class SomeComponent extends Component { class SomeComponent extends Component {
async willStart() { async willStart() {
this.state = useState(); this.state = useState({value: 0});
} }
} }
``` ```
@@ -126,11 +126,64 @@ class Counter extends owl.Component {
### `onMounted` ### `onMounted`
`onMounted` is not an hook, but is a building block designed to help make useful `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 hooks. `onMounted` register a callback, which will be called when the component
is mounted (see example on top of this page). is mounted (see example on top of this page).
### `onWillUnmount` ### `onWillUnmount`
`onWillUnmount` is not an hook, but is a building block designed to help make useful `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 hooks. `onWillUnmount` register a callback, which will be called when the component
is unmounted (see example on top of this page). is unmounted (see example on top of this page).
### `useRef`
The `useRef` hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
tagged by the `t-ref` directive:
```xml
<div>
<div t-ref="someDiv"/>
<SubComponent t-ref="someComponent"/>
</div>
```
In this example, the component will be able to access the `div` and the component
`SubComponent` using the `useRef` hook:
```js
class Parent extends Component {
subRef = useRef("someComponent");
divRef = useRef("someDiv");
someMethod() {
// here, if component is mounted, refs are active:
// - this.divRef.el is the div HTMLElement
// - this.subRef.comp is the instance of the sub component
}
}
```
As shown by the example above, html elements are accessed by using the `el`
key, and components references are accessed with `comp`.
Note: if used on a component, the reference will be set in the `refs`
variable between `willPatch` and `patched`.
The `t-ref` directive also accepts dynamic values with string interpolation
(like the [`t-attf-`](qweb.md#dynamic-attributes) and
`t-component` directives). For example,
```xml
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
```
Here, the references needs to be set like this:
```js
this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");
```
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing `el` or `comp` on it will return `null`.
+2 -2
View File
@@ -11,10 +11,10 @@ For example, this code will display `update` in the console:
```javascript ```javascript
const observer = new owl.Observer(); const observer = new owl.Observer();
observer.notifyCB = () => console.log("update"); observer.notifyCB = () => console.log("update");
const obj = observer.observe( { a: { b: 1 } }); const obj = observer.observe({ a: { b: 1 } });
obj.a.b = 2; obj.a.b = 2;
``` ```
The observer is implemented with the native `Proxy` object. Note that this The observer is implemented with the native `Proxy` object. Note that this
means that it will not work on older browsers. means that it will not work on older browsers.
+1
View File
@@ -17,6 +17,7 @@ owl
onMounted onMounted
onWillUnmount onWillUnmount
useState useState
useRef
router router
Link Link
RouteComponent RouteComponent
+4 -5
View File
@@ -61,10 +61,9 @@ useful to compare various performance metrics on some tasks.
If you want to have `xml` syntax highlighting while using the `xml` helper which If you want to have `xml` syntax highlighting while using the `xml` helper which
helps you define inline templates, there is a VS Code addon `Comment tagged template` helps you define inline templates, there is a VS Code addon `Comment tagged template`
which, if installed, does exactly that. To enable it, you need to add a comment, which, if installed, does exactly that. To enable it, you need to add a comment,
like this: like this:
```js ```js
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// TEMPLATE // TEMPLATE
@@ -79,9 +78,9 @@ const TEMPLATE = xml/* xml */ `
// CODE // CODE
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
class MyComponent extends Component { class MyComponent extends Component {
static template = TEMPLATE; static template = TEMPLATE;
static components = { Sidebar, Content }; static components = { Sidebar, Content };
// rest of component... // rest of component...
} }
``` ```
+6 -10
View File
@@ -85,6 +85,7 @@ interface Internal<T extends Env, Props> {
render: CompiledTemplate | null; render: CompiledTemplate | null;
mountedHandlers: { [key: number]: Function }; mountedHandlers: { [key: number]: Function };
classObj: { [key: string]: boolean } | null; classObj: { [key: string]: boolean } | null;
refs: { [key: string]: Component<T, any> | HTMLElement | undefined } | null;
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -97,6 +98,9 @@ export class Component<T extends Env, Props extends {}> {
static template?: string | null = null; static template?: string | null = null;
static _template?: string | null = null; static _template?: string | null = null;
static _current?: any | null = null; static _current?: any | null = null;
static components = {};
static props?: any;
static defaultProps?: any;
/** /**
* The `el` is the root element of the component. Note that it could be null: * The `el` is the root element of the component. Note that it could be null:
@@ -105,19 +109,10 @@ export class Component<T extends Env, Props extends {}> {
get el(): HTMLElement | null { get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null; return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
} }
static components = {};
env: T; env: T;
props: Props; props: Props;
// type of props is not easily representable in typescript...
static props?: any;
static defaultProps?: any;
refs: {
[key: string]: Component<T, any> | HTMLElement | undefined;
} = {};
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Lifecycle // Lifecycle
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -192,7 +187,8 @@ export class Component<T extends Env, Props extends {}> {
mountedHandlers: {}, mountedHandlers: {},
observer: null, observer: null,
render: null, render: null,
classObj: null classObj: null,
refs: null
}; };
} }
+3 -2
View File
@@ -263,9 +263,10 @@ QWeb.addDirective({
let refExpr = ""; let refExpr = "";
let refKey: string = ""; let refKey: string = "";
if (ref) { if (ref) {
ctx.rootContext.shouldDefineRefs = true;
refKey = `ref${ctx.generateID()}`; refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
refExpr = `context.refs[${refKey}] = w${componentID};`; refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
} }
let transitionsInsertCode = ""; let transitionsInsertCode = "";
if (transition) { if (transition) {
@@ -273,7 +274,7 @@ QWeb.addDirective({
} }
let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`; let finalizeComponentCode = `w${componentID}.${keepAlive ? "unmount" : "destroy"}();`;
if (ref && !keepAlive) { if (ref && !keepAlive) {
finalizeComponentCode += `delete context.refs[${refKey}];`; finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
} }
if (transition) { if (transition) {
finalizeComponentCode = `let finalize = () => { finalizeComponentCode = `let finalize = () => {
+29 -4
View File
@@ -9,9 +9,9 @@ import { Observer } from "./core/observer";
* - useState (reactive state) * - useState (reactive state)
* - onMounted * - onMounted
* - onWillUnmount * - onWillUnmount
* - useRef
*/ */
/** /**
* useState hook * useState hook
* *
@@ -20,7 +20,7 @@ import { Observer } from "./core/observer";
* trigger a rerendering of the current component. * trigger a rerendering of the current component.
*/ */
export function useState<T>(state: T): T { export function useState<T>(state: T): T {
const component: Component<any,any> = Component._current; const component: Component<any, any> = Component._current;
const __owl__ = component.__owl__; const __owl__ = component.__owl__;
if (!__owl__.observer) { if (!__owl__.observer) {
__owl__.observer = new Observer(); __owl__.observer = new Observer();
@@ -34,7 +34,7 @@ export function useState<T>(state: T): T {
* mounted. Note that the component mounted method is called first. * mounted. Note that the component mounted method is called first.
*/ */
export function onMounted(cb) { export function onMounted(cb) {
const component = Component._current; const component: Component<any, any> = Component._current;
const current = component.mounted; const current = component.mounted;
component.mounted = function() { component.mounted = function() {
current.call(component); current.call(component);
@@ -47,10 +47,35 @@ export function onMounted(cb) {
* willUnmounted. Note that the component mounted method is called last. * willUnmounted. Note that the component mounted method is called last.
*/ */
export function onWillUnmount(cb) { export function onWillUnmount(cb) {
const component = Component._current; const component: Component<any, any> = Component._current;
const current = component.willUnmount; const current = component.willUnmount;
component.willUnmount = function() { component.willUnmount = function() {
cb(); cb();
current.call(component); current.call(component);
}; };
} }
/**
* useRef hook
*
* The purpose of this hook is to allow components to get a reference to a sub
* html node or component.
*/
interface Ref {
el: HTMLElement | null;
comp: Component<any, any> | null;
}
export function useRef(name: string): Ref {
const __owl__ = Component._current.__owl__;
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof HTMLElement ? val : null;
},
get comp(): Component<any, any> | null {
const val = __owl__.refs && __owl__.refs[name];
return val instanceof Component ? val : null;
}
};
}
+4
View File
@@ -20,6 +20,7 @@ export class Context {
shouldDefineParent: boolean = false; shouldDefineParent: boolean = false;
shouldDefineQWeb: boolean = false; shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false; shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
shouldDefineResult: boolean = true; shouldDefineResult: boolean = true;
shouldProtectContext: boolean = false; shouldProtectContext: boolean = false;
shouldTrackScope: boolean = false; shouldTrackScope: boolean = false;
@@ -62,6 +63,9 @@ export class Context {
if (this.shouldDefineResult) { if (this.shouldDefineResult) {
this.code.unshift(" let result;"); this.code.unshift(" let result;");
} }
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
if (this.shouldDefineOwner) { if (this.shouldDefineOwner) {
// this is necessary to prevent some directives (t-forach for ex) to // this is necessary to prevent some directives (t-forach for ex) to
// pollute the rendering context by adding some keys in it. // pollute the rendering context by adding some keys in it.
+2 -1
View File
@@ -79,9 +79,10 @@ QWeb.addDirective({
name: "ref", name: "ref",
priority: 95, priority: 95,
atNodeCreation({ ctx, value, addNodeHook }) { atNodeCreation({ ctx, value, addNodeHook }) {
ctx.rootContext.shouldDefineRefs = true;
const refKey = `ref${ctx.generateID()}`; const refKey = `ref${ctx.generateID()}`;
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
addNodeHook("create", `context.refs[${refKey}] = n.elm;`); addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
} }
}); });
+3 -2
View File
@@ -1,6 +1,6 @@
import { Component, Env } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { QWeb } from "../src/qweb/index"; import { QWeb } from "../src/qweb/index";
import { useState } from "../src/hooks"; import { useState, useRef } from "../src/hooks";
import { import {
makeDeferred, makeDeferred,
makeTestFixture, makeTestFixture,
@@ -150,6 +150,7 @@ describe("animations", () => {
); );
class TestWidget extends Widget { class TestWidget extends Widget {
state = useState({ hide: false }); state = useState({ hide: false });
span = useRef("span");
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
@@ -164,7 +165,7 @@ describe("animations", () => {
}); });
await widget.mount(fixture); await widget.mount(fixture);
spanNode = widget.el!.children[0]; spanNode = widget.el!.children[0];
expect(widget.refs.span).toBe(spanNode); expect(widget.span.el).toBe(spanNode);
expect(spanNode.className).toBe("chimay-enter chimay-enter-active"); expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
await def; // wait for the mocked repaint to be done await def; // wait for the mocked repaint to be done
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
@@ -299,6 +299,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let owner = context; let owner = context;
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
@@ -327,7 +328,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
w4 = new W4(parent, props4); w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(extra.fiber, undefined, undefined); def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.__owl__.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.__owl__.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } else {
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined); def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
@@ -359,6 +360,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let owner = context; let owner = context;
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
@@ -387,7 +389,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
w4 = new W4(parent, props4); w4 = new W4(parent, props4);
parent.__owl__.cmap[4] = w4.__owl__.id; parent.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4.__prepare(extra.fiber, undefined, undefined); def3 = w4.__prepare(extra.fiber, undefined, undefined);
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}vnode.data.hook = {create(_, vn){}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;context.__owl__.refs[ref5] = w4;},remove() {},destroy(vn) {w4.destroy();delete context.__owl__.refs[ref5];}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } else {
def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined); def3 = def3 || w4.__updateProps(props4, extra.fiber, undefined, undefined);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
@@ -1552,6 +1554,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
Object.assign(context, extra.fiber.scope); Object.assign(context, extra.fiber.scope);
@@ -1566,7 +1569,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
const ref11 = \`myButton\`; const ref11 = \`myButton\`;
p10.hook = { p10.hook = {
create: (_, n) => { create: (_, n) => {
context.refs[ref11] = n.elm; context.__owl__.refs[ref11] = n.elm;
}, },
}; };
c10.push({text: \`do something\`}); c10.push({text: \`do something\`});
+60 -44
View File
@@ -1,7 +1,7 @@
import { Component, Env } from "../../src/component/component"; import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb"; import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
import { useState } from "../../src/hooks"; import { useState, useRef } from "../../src/hooks";
import { EventBus } from "../../src/core/event_bus"; import { EventBus } from "../../src/core/event_bus";
import { import {
makeDeferred, makeDeferred,
@@ -989,13 +989,15 @@ describe("composition", () => {
}); });
test("t-refs on widget are components", async () => { test("t-refs on widget are components", async () => {
env.qweb.addTemplate("WidgetC", `<div>Hello<t t-ref="mywidgetb" t-component="b"/></div>`);
class WidgetC extends Widget { class WidgetC extends Widget {
static components = { b: WidgetB }; static template = xml`<div>Hello<WidgetB t-ref="mywidgetb" /></div>`;
static components = { WidgetB };
widget = useRef("mywidgetb");
} }
const widget = new WidgetC(env); const widget = new WidgetC(env);
await widget.mount(fixture); await widget.mount(fixture);
expect(widget.refs.mywidgetb instanceof WidgetB).toBe(true); expect(widget.widget.comp).toBeInstanceOf(WidgetB);
}); });
test("t-refs are bound at proper timing", async () => { test("t-refs are bound at proper timing", async () => {
@@ -1008,11 +1010,12 @@ describe("composition", () => {
`; `;
static components = { Widget }; static components = { Widget };
state = useState({ list: <any>[] }); state = useState({ list: <any>[] });
child = useRef("child");
willPatch() { willPatch() {
expect(this.refs.child).toBeUndefined(); expect(this.child.comp).toBeNull();
} }
patched() { patched() {
expect(this.refs.child).not.toBeUndefined(); expect(this.child.comp).not.toBeNull();
} }
} }
@@ -1035,29 +1038,31 @@ describe("composition", () => {
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { Widget }; static components = { Widget };
state = useState({ child1: true, child2: false }); state = useState({ child1: true, child2: false });
child1 = useRef("child1");
child2 = useRef("child2");
count = 0; count = 0;
mounted() { mounted() {
expect(this.refs.child1).toBeDefined(); expect(this.child1.comp).toBeDefined();
expect(this.refs.child2).toBeUndefined(); expect(this.child2.comp).toBeNull();
} }
willPatch() { willPatch() {
if (this.count === 0) { if (this.count === 0) {
expect(this.refs.child1).toBeDefined(); expect(this.child1.comp).toBeDefined();
expect(this.refs.child2).toBeUndefined(); expect(this.child2.comp).toBeNull();
} }
if (this.count === 1) { if (this.count === 1) {
expect(this.refs.child1).toBeDefined(); expect(this.child1.comp).toBeDefined();
expect(this.refs.child2).toBeDefined(); expect(this.child2.comp).toBeDefined();
} }
} }
patched() { patched() {
if (this.count === 0) { if (this.count === 0) {
expect(this.refs.child1).toBeDefined(); expect(this.child1.comp).toBeDefined();
expect(this.refs.child2).toBeDefined(); expect(this.child2.comp).toBeDefined();
} }
if (this.count === 1) { if (this.count === 1) {
expect(this.refs.child1).toBeUndefined(); expect(this.child1.comp).toBeNull();
expect(this.refs.child2).toBeDefined(); expect(this.child2.comp).toBeDefined();
} }
this.count++; this.count++;
} }
@@ -1095,12 +1100,19 @@ describe("composition", () => {
</div>` </div>`
); );
class ParentWidget extends Widget { class ParentWidget extends Widget {
state = useState({ items: [1, 2, 3] });
static components = { Child: Widget }; static components = { Child: Widget };
elem1 = useRef("1");
elem2 = useRef("2");
elem3 = useRef("3");
elem4 = useRef("4");
state = useState({ items: [1, 2, 3] });
} }
const parent = new ParentWidget(env); const parent = new ParentWidget(env);
await parent.mount(fixture); await parent.mount(fixture);
expect(Object.keys(parent.refs)).toEqual(["1", "2", "3"]); expect(parent.elem1.comp).toBeDefined();
expect(parent.elem2.comp).toBeDefined();
expect(parent.elem3.comp).toBeDefined();
expect(parent.elem4.comp).toBeNull();
}); });
test("parent's elm for a children === children's elm, even after rerender", async () => { test("parent's elm for a children === children's elm, even after rerender", async () => {
@@ -1217,36 +1229,37 @@ describe("composition", () => {
}); });
test("sub widget with t-ref and t-keepalive", async () => { test("sub widget with t-ref and t-keepalive", async () => {
env.qweb.addTemplates(` class ChildWidget extends Widget {
<templates> static template = xml`<span>Hello</span>`;
<div t-name="ParentWidget"> }
<t t-if="state.ok"><ChildWidget t-ref="child" t-keepalive="1"/></t>
</div>
<span t-name="ChildWidget">Hello</span>
</templates>`);
class ChildWidget extends Widget {}
class ParentWidget extends Widget { class ParentWidget extends Widget {
state = useState({ ok: true }); static template = xml`
<div>
<t t-if="state.ok"><ChildWidget t-ref="child" t-keepalive="1"/></t>
</div>
`;
static components = { ChildWidget }; static components = { ChildWidget };
state = useState({ ok: true });
child = useRef("child");
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
let child = children(widget)[0]; let child = children(widget)[0];
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
expect(widget.refs.child).toEqual(child); expect(widget.child.comp).toEqual(child);
widget.state.ok = false; widget.state.ok = false;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div></div>"); expect(fixture.innerHTML).toBe("<div></div>");
expect(widget.refs.child).toEqual(child); expect(widget.child.comp).toEqual(child);
widget.state.ok = true; widget.state.ok = true;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>"); expect(fixture.innerHTML).toBe("<div><span>Hello</span></div>");
expect(widget.refs.child).toEqual(child); expect(widget.child.comp).toEqual(child);
}); });
test("sub components rendered in a loop", async () => { test("sub components rendered in a loop", async () => {
@@ -1582,6 +1595,7 @@ describe("class and style attributes with t-component", () => {
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { Child }; static components = { Child };
state = useState({ b: true }); state = useState({ b: true });
child = useRef("child");
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -1593,7 +1607,7 @@ describe("class and style attributes with t-component", () => {
await nextTick(); await nextTick();
expect(span.className).toBe("c d a"); expect(span.className).toBe("c d a");
(<any>widget.refs.child).state.d = false; (widget.child.comp as Child).state.d = false;
await nextTick(); await nextTick();
expect(span.className).toBe("c a"); expect(span.className).toBe("c a");
@@ -1601,7 +1615,7 @@ describe("class and style attributes with t-component", () => {
await nextTick(); await nextTick();
expect(span.className).toBe("c a b"); expect(span.className).toBe("c a b");
(<any>widget.refs.child).state.d = true; (widget.child.comp as Child).state.d = true;
await nextTick(); await nextTick();
expect(span.className).toBe("c a b d"); expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
@@ -1623,6 +1637,7 @@ describe("class and style attributes with t-component", () => {
class ParentWidget extends Widget { class ParentWidget extends Widget {
static components = { Child }; static components = { Child };
state = useState({ b: true }); state = useState({ b: true });
child = useRef("child");
} }
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
@@ -1634,7 +1649,7 @@ describe("class and style attributes with t-component", () => {
await nextTick(); await nextTick();
expect(span.className).toBe("c d a"); expect(span.className).toBe("c d a");
(<any>widget.refs.child).state.d = false; (widget.child.comp as Child).state.d = false;
await nextTick(); await nextTick();
expect(span.className).toBe("c a"); expect(span.className).toBe("c a");
@@ -1642,7 +1657,7 @@ describe("class and style attributes with t-component", () => {
await nextTick(); await nextTick();
expect(span.className).toBe("c a b"); expect(span.className).toBe("c a b");
(<any>widget.refs.child).state.d = true; (widget.child.comp as Child).state.d = true;
await nextTick(); await nextTick();
expect(span.className).toBe("c a b d"); expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot(); expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
@@ -3000,12 +3015,13 @@ describe("t-mounted directive", () => {
test("combined with a t-ref", async () => { test("combined with a t-ref", async () => {
env.qweb.addTemplate("TestWidget", `<div><input t-ref="input" t-mounted="f"/></div>`); env.qweb.addTemplate("TestWidget", `<div><input t-ref="input" t-mounted="f"/></div>`);
class TestWidget extends Widget { class TestWidget extends Widget {
input = useRef("input");
f() {} f() {}
} }
const widget = new TestWidget(env); const widget = new TestWidget(env);
widget.f = jest.fn(); widget.f = jest.fn();
await widget.mount(fixture); await widget.mount(fixture);
expect(widget.refs.input).toBeDefined(); expect(widget.input.el).toBeDefined();
expect(widget.f).toHaveBeenCalledTimes(1); expect(widget.f).toHaveBeenCalledTimes(1);
}); });
}); });
@@ -3233,21 +3249,21 @@ describe("t-slot directive", () => {
}); });
test("refs are properly bound in slots", async () => { test("refs are properly bound in slots", async () => {
env.qweb.addTemplates(` class Dialog extends Widget {
<templates> static template = xml`<span><t t-slot="footer"/></span>`;
<div t-name="Parent"> }
class Parent extends Widget {
static template = xml`
<div>
<span class="counter"><t t-esc="state.val"/></span> <span class="counter"><t t-esc="state.val"/></span>
<Dialog> <Dialog>
<t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t> <t t-set="footer"><button t-ref="myButton" t-on-click="doSomething">do something</button></t>
</Dialog> </Dialog>
</div> </div>
<span t-name="Dialog"><t t-slot="footer"/></span> `;
</templates>
`);
class Dialog extends Widget {}
class Parent extends Widget {
static components = { Dialog }; static components = { Dialog };
state = useState({ val: 0 }); state = useState({ val: 0 });
button = useRef("myButton");
doSomething() { doSomething() {
this.state.val++; this.state.val++;
} }
@@ -3259,7 +3275,7 @@ describe("t-slot directive", () => {
'<div><span class="counter">0</span><span><button>do something</button></span></div>' '<div><span class="counter">0</span><span><button>do something</button></span></div>'
); );
(<any>parent.refs.myButton).click(); parent.button.el!.click();
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe( expect(fixture.innerHTML).toBe(
+19 -1
View File
@@ -1,6 +1,6 @@
import { makeTestEnv, makeTestFixture, nextTick } from "./helpers"; import { makeTestEnv, makeTestFixture, nextTick } from "./helpers";
import { Component, Env } from "../src/component/component"; import { Component, Env } from "../src/component/component";
import { useState, onMounted, onWillUnmount } from "../src/hooks"; import { useState, onMounted, onWillUnmount, useRef } from "../src/hooks";
import { xml } from "../src/tags"; import { xml } from "../src/tags";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -129,4 +129,22 @@ describe("hooks", () => {
"hook:willunmount1" "hook:willunmount1"
]); ]);
}); });
test("useRef hook", async () => {
class Counter extends Component<any, any> {
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
button = useRef("button");
value = 0;
increment() {
this.value++;
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
}
}
const counter = new Counter(env);
await counter.mount(fixture);
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
counter.increment();
await nextTick();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
});
}); });
+6 -3
View File
@@ -1915,6 +1915,7 @@ exports[`t-raw variable 1`] = `
exports[`t-ref can get a dynamic ref on a node 1`] = ` exports[`t-ref can get a dynamic ref on a node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
@@ -1925,7 +1926,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
const ref3 = \`myspan\${context['id']}\`; const ref3 = \`myspan\${context['id']}\`;
p2.hook = { p2.hook = {
create: (_, n) => { create: (_, n) => {
context.refs[ref3] = n.elm; context.__owl__.refs[ref3] = n.elm;
}, },
}; };
return vn1; return vn1;
@@ -1935,6 +1936,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
exports[`t-ref can get a ref on a node 1`] = ` exports[`t-ref can get a ref on a node 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context.__owl__.refs = context.__owl__.refs || {};
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
@@ -1945,7 +1947,7 @@ exports[`t-ref can get a ref on a node 1`] = `
const ref3 = \`myspan\`; const ref3 = \`myspan\`;
p2.hook = { p2.hook = {
create: (_, n) => { create: (_, n) => {
context.refs[ref3] = n.elm; context.__owl__.refs[ref3] = n.elm;
}, },
}; };
return vn1; return vn1;
@@ -1955,6 +1957,7 @@ exports[`t-ref can get a ref on a node 1`] = `
exports[`t-ref refs in a loop 1`] = ` exports[`t-ref refs in a loop 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
context.__owl__.refs = context.__owl__.refs || {};
context = Object.create(context); context = Object.create(context);
var h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
@@ -1981,7 +1984,7 @@ exports[`t-ref refs in a loop 1`] = `
const ref6 = (context['item']); const ref6 = (context['item']);
p5.hook = { p5.hook = {
create: (_, n) => { create: (_, n) => {
context.refs[ref6] = n.elm; context.__owl__.refs[ref6] = n.elm;
}, },
}; };
var _7 = context['item']; var _7 = context['item'];
+4 -4
View File
@@ -1092,14 +1092,14 @@ describe("t-ref", () => {
test("can get a ref on a node", () => { test("can get a ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`); qweb.addTemplate("test", `<div><span t-ref="myspan"/></div>`);
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs }); renderToDOM(qweb, "test", { __owl__: { refs } });
expect(refs.myspan.tagName).toBe("SPAN"); expect(refs.myspan.tagName).toBe("SPAN");
}); });
test("can get a dynamic ref on a node", () => { test("can get a dynamic ref on a node", () => {
qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`); qweb.addTemplate("test", `<div><span t-ref="myspan{{id}}"/></div>`);
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs, id: 3 }); renderToDOM(qweb, "test", { id: 3, __owl__: { refs } });
expect(refs.myspan3.tagName).toBe("SPAN"); expect(refs.myspan3.tagName).toBe("SPAN");
}); });
@@ -1114,7 +1114,7 @@ describe("t-ref", () => {
</div>` </div>`
); );
let refs: any = {}; let refs: any = {};
renderToDOM(qweb, "test", { refs, items: [1, 2, 3] }); renderToDOM(qweb, "test", { items: [1, 2, 3], __owl__: { refs } });
expect(Object.keys(refs)).toEqual(["1", "2", "3"]); expect(Object.keys(refs)).toEqual(["1", "2", "3"]);
}); });
}); });
@@ -1333,5 +1333,5 @@ describe("properly support svg", () => {
expect(renderToString(qweb, "test")).toBe( expect(renderToString(qweb, "test")).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>` `<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
); );
}); });
}); });
+5 -4
View File
@@ -1,6 +1,6 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js"; import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const useState = owl.hooks.useState; const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Likes Counter Widget // Likes Counter Widget
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -34,6 +34,7 @@ class Message extends owl.Component {
class App extends owl.Component { class App extends owl.Component {
static components = { Message }; static components = { Message };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false }); state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() { mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`); this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
@@ -130,12 +131,12 @@ class App extends owl.Component {
div.classList.add("bold"); div.classList.add("bold");
} }
div.textContent = `> ${str}`; div.textContent = `> ${str}`;
this.refs.log.appendChild(div); this.logRef.el.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight; this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
} }
clearLog() { clearLog() {
this.refs.log.innerHTML = ""; this.logRef.el.innerHTML = "";
} }
} }
+7 -6
View File
@@ -1,5 +1,5 @@
import { SAMPLES } from "./samples.js"; import { SAMPLES } from "./samples.js";
const useState = owl.hooks.useState; const {useState, useRef} = owl.hooks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Constants, helpers, utils // Constants, helpers, utils
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -163,11 +163,11 @@ class TabbedEditor extends owl.Component {
this.sessions = {}; this.sessions = {};
this._setupSessions(props); this._setupSessions(props);
this.editor = null; this.editorNode = useRef("editor");
} }
mounted() { mounted() {
this.editor = this.editor || ace.edit(this.refs.editor); this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1); this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px"); this.editor.setFontSize("12px");
@@ -264,13 +264,14 @@ class App extends owl.Component {
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true); this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true); this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true); this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.content = useRef("content");
} }
displayError(error) { displayError(error) {
this.state.error = error; this.state.error = error;
if (error) { if (error) {
setTimeout(() => { setTimeout(() => {
this.refs.content.innerHTML = ""; this.content.el.innerHTML = "";
}); });
return; return;
} }
@@ -296,8 +297,8 @@ class App extends owl.Component {
} else { } else {
this.state.error = false; this.state.error = false;
} }
this.refs.content.innerHTML = ""; this.content.el.innerHTML = "";
this.refs.content.appendChild(subiframe); this.content.el.appendChild(subiframe);
} }
setSample(ev) { setSample(ev) {
+22 -6
View File
@@ -325,6 +325,7 @@ const TODO_APP_STORE = `// This example is an implementation of the TodoList app
// In this implementation, we use the owl Store class to manage the state. It // In this implementation, we use the owl Store class to manage the state. It
// is very similar to the VueX store. // is very similar to the VueX store.
const { Component, useState } = owl; const { Component, useState } = owl;
const { useRef } = owl.hooks;
const ENTER_KEY = 13; const ENTER_KEY = 13;
const ESC_KEY = 27; const ESC_KEY = 27;
@@ -397,6 +398,7 @@ function makeStore() {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class TodoItem extends Component { class TodoItem extends Component {
state = useState({ isEditing: false }); state = useState({ isEditing: false });
inputRef = useRef("input");
removeTodo() { removeTodo() {
this.env.store.dispatch("removeTodo", this.props.id); this.env.store.dispatch("removeTodo", this.props.id);
@@ -411,9 +413,9 @@ class TodoItem extends Component {
} }
focusInput() { focusInput() {
this.refs.input.value = ""; this.inputRef.el.value = "";
this.refs.input.focus(); this.inputRef.el.focus();
this.refs.input.value = this.props.title; this.inputRef.el.value = this.props.title;
} }
handleKeyup(ev) { handleKeyup(ev) {
@@ -1380,6 +1382,7 @@ const WMS = `// This example is slightly more complex than usual. We demonstrate
// - better heuristic for initial window position // - better heuristic for initial window position
// - ... // - ...
const { Component, useState } = owl; const { Component, useState } = owl;
const { useRef } = owl.hooks;
class HelloWorld extends Component {} class HelloWorld extends Component {}
@@ -1392,14 +1395,17 @@ class Counter extends Component {
} }
class Window extends Component { class Window extends Component {
get style() { get style() {
let { width, height, top, left, zindex } = this.props.info; let { width, height, top, left, zindex } = this.props.info;
return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`; return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
} }
close() { close() {
this.trigger("close-window", { id: this.props.info.id }); this.trigger("close-window", { id: this.props.info.id });
} }
startDragAndDrop(ev) { startDragAndDrop(ev) {
this.updateZIndex(); this.updateZIndex();
this.el.classList.add('dragging'); this.el.classList.add('dragging');
@@ -1425,6 +1431,7 @@ class Window extends Component {
self.trigger("set-window-position", options); self.trigger("set-window-position", options);
} }
} }
updateZIndex() { updateZIndex() {
this.trigger("update-z-index", { id: this.props.info.id }); this.trigger("update-z-index", { id: this.props.info.id });
} }
@@ -1435,20 +1442,26 @@ class WindowManager extends Component {
windows = []; windows = [];
nextId = 1; nextId = 1;
currentZindex = 1; currentZindex = 1;
nextLeft = 0;
nextTop = 0;
addWindow(name) { addWindow(name) {
const info = this.env.windows.find(w => w.name === name); const info = this.env.windows.find(w => w.name === name);
this.nextLeft = this.nextLeft + 30;
this.nextTop = this.nextTop + 30;
this.windows.push({ this.windows.push({
id: this.nextId++, id: this.nextId++,
title: info.title, title: info.title,
width: info.defaultWidth, width: info.defaultWidth,
height: info.defaultHeight, height: info.defaultHeight,
top: 0, top: this.nextTop,
left: 0, left: this.nextLeft,
zindex: this.currentZindex++, zindex: this.currentZindex++,
component: info.component component: info.component
}); });
this.render(); this.render();
} }
closeWindow(ev) { closeWindow(ev) {
const id = ev.detail.id; const id = ev.detail.id;
delete this.constructor.components[id]; delete this.constructor.components[id];
@@ -1456,12 +1469,14 @@ class WindowManager extends Component {
this.windows.splice(index, 1); this.windows.splice(index, 1);
this.render(); this.render();
} }
setWindowPosition(ev) { setWindowPosition(ev) {
const id = ev.detail.id; const id = ev.detail.id;
const w = this.windows.find(w => w.id === id); const w = this.windows.find(w => w.id === id);
w.top = ev.detail.top; w.top = ev.detail.top;
w.left = ev.detail.left; w.left = ev.detail.left;
} }
updateZIndex(ev) { updateZIndex(ev) {
const id = ev.detail.id; const id = ev.detail.id;
const w = this.windows.find(w => w.id === id); const w = this.windows.find(w => w.id === id);
@@ -1472,9 +1487,10 @@ class WindowManager extends Component {
class App extends Component { class App extends Component {
static components = { WindowManager }; static components = { WindowManager };
wmRef = useRef("wm");
addWindow(name) { addWindow(name) {
this.refs.wm.addWindow(name); this.wmRef.comp.addWindow(name);
} }
} }